Author SHA1 Message Date
Tirsvad 049075957a Merge origin/mil-006-license: .env stays optional, config.env is required 2026-10-07 14:34:04 +08:00
Tirsvad ed79da4e34 Merge main into mil-006-license 2026-10-07 14:19:41 +08:00
Tirsvad fcc155a8ff Merge pull request 'Implement MIL-007: qc submodules fetched, start through a link, default configuration files' (#53) from mil-007-impl into mil-006-license
Reviewed-on: #53
2026-10-07 08:17:22 +02:00
Tirsvad 3204e20cac MIL-007: fix the new tests and document starting from any folder
- README: start from the folder where the project is created, make the script a global command, where config.env and .env are read from and the confirmation
- Tests: qc tests set up the temp directory and force a real failure; the default-files test runs from a folder without files of its own; the link test allows for path aliases; the work directory cleanup deletes links

Task: MIL-007#3
Task: MIL-007#4
Refs #49
Refs #51
2026-10-07 14:09:27 +08:00
Tirsvad 1a283430b3 WIP MIL-007: fetch the framework's qc, follow links, default configuration files
- git submodule update --init --recursive after adding or reusing the framework
- Follow links to the script so SCRIPT_DIR and the checkout are the real ones
- config.env and .env: --config/--env, else ./ in the working folder, else the checkout's
- Name the files used; a file from the working folder needs a yes before any request
- MIL-007 accepted; test fixtures for the qc checklists; tests/test-launch.sh

Work in progress: the README section is not written yet, and four tests of
test-launch.sh fail and two shellcheck warnings remain; they are fixed next.

Task: MIL-007#1
Task: MIL-007#2
Refs #47
Refs #48
Refs #51
2026-10-07 13:40:42 +08:00
Tirsvad 875dfba1b2 Use a public project in the test that reuses a repository holding LICENSE and README.md
A private project with GitHub no longer gets a license, so only a public one expects it.

Task: MIL-006#4
Refs #50
2026-10-07 13:28:47 +08:00
Tirsvad e960dd9da8 Set the project license in config.env; AGPL-3.0 default only for a public GitHub project
- PROJECT_LICENSE (a Gitea license key, or none) is read, checked and never asked
- Without it AGPL-3.0 applies only when GitHub is chosen and the project is public
- The license is applied on Gitea with or without GitHub, and checked against the server first
- Plan and summary name the license and where it came from
- MIL-006 accepted; README and config.env.example document the key
- Tests: new test-license.sh; existing tests use a public project where they expect AGPL-3.0

Task: MIL-006#1
Task: MIL-006#2
Task: MIL-006#3
Task: MIL-006#4
Refs #44
Refs #45
Refs #46
Refs #50
2026-10-07 13:08:49 +08:00
Tirsvad b324acbfae Resolve pending commit links for the default configuration files 2026-10-07 12:42:08 +08:00
Tirsvad 0ab50068bf Plan the default configuration files: working folder first, then the checkout
- --config and --env, else ./config.env and ./.env, else the checkout's
- Files named before any request; a file from the working folder needs a yes
- UC-002, US-002, MIL-007 criteria 9 and 10, models and contracts updated
- Review record RC-029
2026-10-07 12:42:05 +08:00
Tirsvad 88b4e5ce6c Resolve pending commit links for the task renames 2026-10-07 12:38:02 +08:00
Tirsvad be759e3832 Give the test tasks of MIL-006 and MIL-007 their own titles
The sync matches issues by title, so Test every case moved issue #39 of MIL-005.
2026-10-07 12:38:01 +08:00
Tirsvad fe6e5a4ba0 Merge pull request 'Plan MIL-006 and MIL-007 and UC-002: public-only AGPL default, qc fetch, global command' (#43) from mil-006-plan into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 3s
Reviewed-on: #43
2026-10-07 06:34:56 +02:00
Tirsvad 250df38d79 Resolve pending commit links for MIL-007, UC-002 and the license rule 2026-10-07 12:31:07 +08:00
Tirsvad 1cd27f77ed Plan MIL-007 and UC-002; AGPL-3.0 default only for a public GitHub project
- MIL-006: the AGPL-3.0 default needs GitHub and a public project
- MIL-007: fetch the framework's own submodules (qc); start through a command link; README usage
- UC-002 with SSD, DM, OC, SD and DCD; US-001.07 and US-002
- Reconcile the project DM, DCD, dictionary, use case diagram and traceability matrix
- Review records RC-022 to RC-028
2026-10-07 12:30:58 +08:00
Tirsvad ea7e25b65d Merge pull request 'Plan MIL-006: the project license set in config.env' (#42) from mil-006-plan into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 3s
Reviewed-on: #42
2026-10-06 08:16:43 +02:00
Tirsvad aca4eed377 Merge pull request 'MIL-005: ask for missing credentials and create the project's own .env' (#41) from mil-005-credentials into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 4s
Reviewed-on: #41
2026-10-06 08:16:31 +02:00
TirsvadandClaude Sonnet 5.5 5b66eff809 Resolve pending commit links for the MIL-006 plan
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 13:43:39 +08:00
TirsvadandClaude Sonnet 5.5 d773fa91df Plan MIL-006: the project license set in config.env
Add objective 10 to the Business Case, US-001.06 and the use case, SSD, OC,
SD, DM-001, DM-002, dictionary, DCD-001 and DCD-002 changes: a license set
in config.env (PROJECT_LICENSE, a Gitea license key or none) applies with or
without GitHub; when absent AGPL-3.0 applies only when GitHub is chosen. It
is never asked, and a license the server does not offer stops the run
before anything is created.

Add milestone MIL-006 (8 Go/No-Go criteria, 4 tasks) and its phase in the
Project Plan. All new rows are Proposed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 13:43:35 +08:00
TirsvadandClaude Sonnet 5.5 05a7159c18 Ask for missing credentials and create the project's own .env
.env becomes optional. A credential it does not provide (an absent file, an
absent or empty key) is asked, without echo: GITEA_TOKEN at the start,
GITHUB_PAT and GITHUB_USER once GitHub is chosen. An invalid value is asked
again and never shown; when input ends the run stops before any request.
Asked tokens are registered for redaction at once.

After the local project exists the script asks (default no) whether to
create a .env in it. On a yes it holds only the needed keys, is created
private (mode 600) from the start, is excluded from git through
.git/info/exclude (no tracked file changes), is never replaced without a
second yes and is never written when git tracks it. The summary names the
keys, never the values.

New library files credentials.sh and envfile.sh; README, .env.example and
the security decisions updated; tests cover every case.

Task: MIL-005#1
Task: MIL-005#2
Task: MIL-005#3
Task: MIL-005#4
Task: MIL-005#5
Closes #35
Closes #36
Closes #37
Closes #38
Closes #39

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 13:29:01 +08:00
Tirsvad 3804ef7556 Merge pull request 'Add the Design Class Diagrams and plan MIL-005 (credentials asked, project .env)' (#40) from dcd-uc-001 into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 4s
Reviewed-on: #40
2026-10-06 07:01:40 +02:00
TirsvadandClaude Sonnet 5.5 7d202a34dc Resolve pending commit links for the MIL-005 plan
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:47:33 +08:00
TirsvadandClaude Sonnet 5.5 ded26a658c Plan MIL-005: ask for missing credentials and create the project .env
Add objective 9 to the Business Case (and amend objective 6 and success
criterion 1: a token may be written only to the new project's .env after a
yes), US-001.05, UC-001 extensions 2b, 9c and 9d, with the SSD, OC, SD,
DM-001, DM-002 and dictionary in step. Add the classes CredentialCollector,
EnvFileWriter and EnvFile to DCD-001 and DCD-002, and both ends'
multiplicities to every association. Restore three lines of SD-001 damaged
by an earlier edit.

Add milestone MIL-005 (9 Go/No-Go criteria, 5 tasks) and its phase in the
Project Plan. Accept the planning set and the two DCDs; reviews recorded in
RC-020 and RC-021.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:47:26 +08:00
TirsvadandClaude Sonnet 5.5 4c9a1af719 Resolve pending commit links for the DCDs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:35:47 +08:00
TirsvadandClaude Sonnet 5.5 f4d611b77c Add the Design Class Diagrams DCD-001 and DCD-002, align SD-001
- DCD-001 (UC-001): design classes refining DM-001, with class table,
  method traceability to OC-001 and SD-001, patterns and dependency check,
  and the mapping of each class to src/lib.
- DCD-002: the consolidated project-level model, created from DCD-001.
- SD-001: messages aligned with the DCD method signatures; cites DCD-001.
- Dictionary, registry and traceability matrix updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:35:45 +08:00
Tirsvad cf265e8f66 Merge pull request 'MIL-004: project details preset in config.env (with the e2e fixes and the plan)' (#34) from mil-004-configurable-details into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: #34
2026-10-05 18:28:47 +02:00
Tirsvad c21a3c50f0 Merge pull request 'MIL-003: local project, framework submodule, hooks, templates and README' (#26) from mil-003-scaffold-and-release into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 4s
Reviewed-on: #26
2026-10-05 09:55:13 +02:00
Tirsvad 5584ddf397 Merge pull request 'MIL-002: GitHub and Gitea steps (dry run, repositories, push mirror) and split into library files' (#25) from mil-002-repositories-and-mirror into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 4s
Reviewed-on: #25
2026-10-05 09:33:06 +02:00
61 changed files with 3734 additions and 255 deletions
+5 -2
View File
@@ -1,6 +1,9 @@
# RepoFoundry credentials. Placeholders only: never put a real value in this # RepoFoundry credentials. Placeholders only: never put a real value in this
# file or commit one. # file or commit one.
# #
# Everything in this file is optional: a credential that is missing here is
# asked for when the script runs (the token is not echoed).
#
# Copy this file to .env, fill in the values and keep it private # Copy this file to .env, fill in the values and keep it private
# (chmod 600 .env on Linux and macOS). .env is ignored by git. The file is # (chmod 600 .env on Linux and macOS). .env is ignored by git. The file is
# read as plain KEY=VALUE lines and never executed. Values may be wrapped in # read as plain KEY=VALUE lines and never executed. Values may be wrapped in
@@ -13,7 +16,7 @@
GITHUB_PAT= GITHUB_PAT=
######################################## ########################################
# Secrets for workframe # Secrets for framework
######################################## ########################################
# GitHub account the token belongs to. It identifies who authenticates; it is # GitHub account the token belongs to. It identifies who authenticates; it is
@@ -21,6 +24,6 @@ GITHUB_PAT=
# belong to an organization. # belong to an organization.
GITHUB_USER= GITHUB_USER=
# Gitea access token (required). It needs permission to create repositories # Gitea access token. It needs permission to create repositories
# for the chosen owner and to manage the repository's push mirror. # for the chosen owner and to manage the repository's push mirror.
GITEA_TOKEN= GITEA_TOKEN=
+116 -14
View File
@@ -59,13 +59,73 @@ src/create-project.sh --help
Nothing has to be installed system-wide: the script runs from the checkout and Nothing has to be installed system-wide: the script runs from the checkout and
loads its own files from `src/lib/`. loads its own files from `src/lib/`.
### Run it from the folder where the project is to be created
The new project is created under the folder you start the script in: the
default directory is `./<repository name>`. Go to the folder that should hold
the project, then start the script from there, by its path or by a global
command (below):
```bash
cd ~/work # the folder that will hold my-app
~/src/RepoFoundry/src/create-project.sh # creates ~/work/my-app
```
### Make it a global command
Put a link to the script in a folder that is on your `PATH`. The script
follows the link to the checkout, so it still finds its own files there.
Linux, macOS and Git Bash on Windows (run this once, from the checkout):
```bash
mkdir -p ~/bin
ln -s "$PWD/src/create-project.sh" ~/bin/repo-foundry
```
If `~/bin` is not on your `PATH` yet, add it and open a new shell:
```bash
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc # ~/.zshrc on macOS
```
Check that it works, from any folder:
```bash
cd ~/work
repo-foundry --version # prints the name and version
repo-foundry # a dry run that creates nothing
```
On Windows, Git Bash makes a *copy* instead of a link unless symbolic links
are allowed (Developer Mode, or an administrator shell). Either allow them and
run `export MSYS=winsymlinks:nativestrict` before the `ln -s`, or use an alias
in `~/.bashrc`, which works because the script finds its own folder:
```bash
alias repo-foundry='bash /c/Users/me/RepoFoundry/src/create-project.sh'
```
## Configuration ## Configuration
The script reads two plain files from the project root. They are **parsed, The script reads two plain files. They are **parsed,
never executed** (`source` is not used): only `KEY=VALUE` lines with known keys never executed** (`source` is not used): only `KEY=VALUE` lines with known keys
are accepted, and anything else stops the run with a message that names the key are accepted, and anything else stops the run with a message that names the key
and the line, never the value. and the line, never the value.
Each file is chosen on its own, in this order:
1. the file named with `--config` or `--env`;
2. `./config.env` or `./.env` in the folder you start the script in;
3. `config.env` or `.env` in the checkout (next to `src/`).
The script names the files it uses before it contacts any host. A file taken
from the folder you started in is also confirmed: the script shows the file
names and the Gitea address and asks for a yes (default no) before the first
request, because a `config.env` in a folder you do not control could point
Gitea at another host and so send your token there. A file you name with
`--config` or `--env`, or the checkout's own, is not asked about.
```bash ```bash
cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL) cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL)
cp .env.example .env # credentials: keep private cp .env.example .env # credentials: keep private
@@ -103,6 +163,7 @@ A detail that is set is used and not asked; the summary marks it with
| `GITHUB_OWNER` | GitHub user or organization | letters, digits, `-`; used only when GitHub is used | | `GITHUB_OWNER` | GitHub user or organization | letters, digits, `-`; used only when GitHub is used |
| `PROJECT_DIRECTORY` | local directory | not empty, not starting with `-` | | `PROJECT_DIRECTORY` | local directory | not empty, not starting with `-` |
| `ENABLE_PLAN_GATE` | enable the plan gate | `yes` or `no` | | `ENABLE_PLAN_GATE` | enable the plan gate | `yes` or `no` |
| `PROJECT_LICENSE` | license of the project | a Gitea license key (letters, digits, `.`, `+`, `-`; at most 64), such as `AGPL-3.0` or `MIT`, or `none` |
- A key that is present counts as set, even when its value is empty. Only - A key that is present counts as set, even when its value is empty. Only
`PROJECT_DESCRIPTION` may be empty (no description); an empty value for any `PROJECT_DESCRIPTION` may be empty (no description); an empty value for any
@@ -111,7 +172,12 @@ A detail that is set is used and not asked; the summary marks it with
key. The script never falls back to asking for it. key. The script never falls back to asking for it.
- `USE_GITHUB=no` skips the GitHub owner and every GitHub step; a - `USE_GITHUB=no` skips the GitHub owner and every GitHub step; a
`GITHUB_OWNER` set at the same time is ignored, with a warning. `GITHUB_OWNER` set at the same time is ignored, with a warning.
- Only these eight details can be set. The confirmations stay questions that - `PROJECT_LICENSE` is never asked. When set, that license is put on the Gitea
repository with or without GitHub, and `none` means no license. When absent,
AGPL-3.0 is applied only if GitHub is used **and** the project is public;
a private project, or one without GitHub, gets no license. The Gitea server
must offer the license, or the run stops before anything is created.
- Only these nine details can be set. The confirmations stay questions that
default to no: create now, reusing an existing repository, an existing default to no: create now, reusing an existing repository, an existing
directory, `core.hooksPath` and replacing a template file. directory, `core.hooksPath` and replacing a template file.
- These keys are accepted in `config.env` only, never in `.env`. - These keys are accepted in `config.env` only, never in `.env`.
@@ -119,7 +185,7 @@ A detail that is set is used and not asked; the summary marks it with
value there. Put a description that contains ` #` in double quotes, for value there. Put a description that contains ` #` in double quotes, for
example `PROJECT_DESCRIPTION="Tool for #mirrors"`. example `PROJECT_DESCRIPTION="Tool for #mirrors"`.
With all eight set, a run asks only the confirmations: With all of them set, a run asks only the confirmations:
```bash ```bash
src/create-project.sh --apply # asks only "Create these now (y/n) [n]" src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
@@ -129,14 +195,40 @@ src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
| Key | Meaning | | Key | Meaning |
| --- | --- | | --- | --- |
| `GITEA_TOKEN` | Gitea access token (required) | | `GITEA_TOKEN` | Gitea access token |
| `GITHUB_PAT` | GitHub personal access token (only when you choose GitHub) | | `GITHUB_PAT` | GitHub personal access token (only when you choose GitHub) |
| `GITHUB_USER` | the GitHub account the token belongs to; only a default for the owner prompt | | `GITHUB_USER` | the GitHub account the token belongs to (only when you choose GitHub) |
`.env` is ignored by git. The script warns if it is readable by other users or `.env` is ignored by git. The script warns if it is readable by other users or
not ignored by git. See [Token permissions](#token-permissions) for what each not ignored by git. See [Token permissions](#token-permissions) for what each
token needs. token needs.
`.env` is optional, and so is each key in it. A credential that is not
provided (the file is missing, the key is absent or its value is empty) is
asked for: the Gitea token at the start, the GitHub token and account name once
you choose GitHub. A token is read without echo and checked like one read from
`.env`; a refused value is asked again and never shown. If input ends before a
valid value is entered, the run stops before any request to a host.
### The project's own `.env`
When the project exists, the script asks whether to create a `.env` in it
(default no). On a yes the file holds only the credentials the project needs:
`GITEA_TOKEN`, and `GITHUB_PAT` and `GITHUB_USER` when you chose GitHub, as read
from your `.env` or typed.
- The file is created readable by you only (mode 600), never readable by
others even for a moment, and is never written by anything else.
- Git ignores it: the script adds `.env` to `.git/info/exclude` of the new
project. No tracked file changes and nothing is committed.
- An existing `.env` in the project is never replaced without a second yes, and
a `.env` that git already tracks is never written.
- The summary names the keys, never the values.
This is the one place the script writes a token to disk. It is plain text: keep
the project directory private, do not copy the file around, and say no if you
do not need it. Tokens are written nowhere else.
## Usage ## Usage
```bash ```bash
@@ -145,6 +237,11 @@ src/create-project.sh --apply # creates everything after a final yes
src/create-project.sh --config /path/to/config.env --env /path/to/.env src/create-project.sh --config /path/to/config.env --env /path/to/.env
``` ```
With a global command (see [Installation](#installation)) the same commands are
`repo-foundry`, `repo-foundry --apply` and so on, started from the folder that
should hold the project. Without `--config` and `--env` the files are looked
for as described under [Configuration](#configuration).
The script asks for, in this order: repository name, description, visibility, The script asks for, in this order: repository name, description, visibility,
Gitea owner, whether to also create a GitHub repository (and its owner), the Gitea owner, whether to also create a GitHub repository (and its owner), the
local directory and whether to enable the plan gate (a detail set in local directory and whether to enable the plan gate (a detail set in
@@ -153,8 +250,8 @@ with read-only requests and prints a plan:
```text ```text
Plan: Plan:
Gitea repository : create (private) with the AGPL-3.0 license https://git.example.org/Team/my-app Gitea repository : create (public) with the AGPL-3.0 license (default: GitHub and a public project) https://git.example.org/Team/my-app
GitHub repository : create (private), empty https://github.com/acme/my-app GitHub repository : create (public), empty https://github.com/acme/my-app
Push mirror : Gitea -> GitHub every 10m0s Push mirror : Gitea -> GitHub every 10m0s
Local project : create ./my-app (new directory), git on main, no commit Local project : create ./my-app (new directory), git on main, no commit
Local origin : will use SSH (the SSH test passed) Local origin : will use SSH (the SSH test passed)
@@ -167,7 +264,7 @@ Without `--apply` that is all that happens. With `--apply` the script asks
"Create these now" (default no) and then creates, in this order: "Create these now" (default no) and then creates, in this order:
1. the GitHub repository (empty), if chosen; 1. the GitHub repository (empty), if chosen;
2. the Gitea repository (with the AGPL-3.0 license if GitHub was chosen); 2. the Gitea repository (with the license that applies: `PROJECT_LICENSE`, or AGPL-3.0 for a public project with GitHub);
3. the push mirror Gitea -> GitHub, and a request for its first sync; 3. the push mirror Gitea -> GitHub, and a request for its first sync;
4. the local directory, `git init` on `main`, the `origin` remote (and `github` 4. the local directory, `git init` on `main`, the `origin` remote (and `github`
if chosen), and, if the Gitea repository holds the license commit, that if chosen), and, if the Gitea repository holds the license commit, that
@@ -181,10 +278,11 @@ hooks refuse commits on `main`.
### Choices ### Choices
- **GitHub or not.** Choosing GitHub also applies the AGPL-3.0 license to the - **GitHub or not.** Choosing GitHub sets up the mirror. For a public project it
Gitea repository (so it is not empty) and sets up the mirror. Without GitHub also applies the AGPL-3.0 license to the Gitea repository (so it is not
the Gitea repository is empty and has no license, and `GITHUB_PAT` is not empty), unless `PROJECT_LICENSE` says otherwise. A private project with
needed. GitHub gets no license by default. Without GitHub the Gitea repository has
no license unless `PROJECT_LICENSE` sets one, and `GITHUB_PAT` is not needed.
- **Owners.** The Gitea owner and the GitHub owner are chosen separately and - **Owners.** The Gitea owner and the GitHub owner are chosen separately and
may be a user or an organization. `GITHUB_USER` is only the suggested default may be a user or an organization. `GITHUB_USER` is only the suggested default
for the GitHub owner prompt; it identifies who authenticates. for the GitHub owner prompt; it identifies who authenticates.
@@ -264,7 +362,9 @@ script stops before it creates anything when a preflight check is refused.
## Security decisions ## Security decisions
- **Tokens never appear** in output, logs, remote URLs, `.git/config`, - **Tokens never appear** in output, logs, remote URLs, `.git/config`,
`.gitmodules`, command lines or leftover files. They go to `curl` through a `.gitmodules`, command lines or leftover files, and are written to disk only
in the new project's own `.env`, after a yes (see
[The project's own `.env`](#the-projects-own-env)). They go to `curl` through a
private configuration file that is removed right after the request, and to private configuration file that is removed right after the request, and to
`git` (HTTPS fetch only) through a `GIT_ASKPASS` helper and the environment `git` (HTTPS fetch only) through a `GIT_ASKPASS` helper and the environment
of that one command. Output is filtered, so even a server message that echoes of that one command. Output is filtered, so even a server message that echoes
@@ -368,11 +468,13 @@ file. The files are loaded from that directory only, by a fixed path.
| `json.sh` | the little JSON the script reads and writes | | `json.sh` | the little JSON the script reads and writes |
| `http.sh` | the one place that runs `curl`; tokens stay off the command line | | `http.sh` | the one place that runs `curl`; tokens stay off the command line |
| `api.sh` | GitHub and Gitea API calls and reporting a refused call | | `api.sh` | GitHub and Gitea API calls and reporting a refused call |
| `prompts.sh` | interactive questions with validation | | `prompts.sh` | interactive questions with validation (secrets are read without echo) |
| `credentials.sh` | asking for a credential that `.env` does not provide |
| `project.sh` | the project details: asking for them and showing them | | `project.sh` | the project details: asking for them and showing them |
| `hosts.sh` | names, links and remote addresses of the repositories | | `hosts.sh` | names, links and remote addresses of the repositories |
| `preflight.sh` | read-only checks of both hosts | | `preflight.sh` | read-only checks of both hosts |
| `steps.sh` | the outcome of each step and the final report | | `steps.sh` | the outcome of each step and the final report |
| `envfile.sh` | the new project's own `.env`: created private, ignored by git, never replaced without a yes |
| `plan.sh` | printing what the script is about to do | | `plan.sh` | printing what the script is about to do |
| `repositories.sh` | creating the GitHub and Gitea repositories | | `repositories.sh` | creating the GitHub and Gitea repositories |
| `mirror.sh` | the Gitea to GitHub push mirror | | `mirror.sh` | the Gitea to GitHub push mirror |
+6
View File
@@ -46,6 +46,12 @@ GITEA_API_URL=https://<your gitea instance>/api/v1
#PROJECT_DIRECTORY=./my-project #PROJECT_DIRECTORY=./my-project
#ENABLE_PLAN_GATE=no # yes or no #ENABLE_PLAN_GATE=no # yes or no
# Optional. The license of the project, as a Gitea license key (AGPL-3.0, MIT,
# ...), or none for no license. It is never asked. When absent, AGPL-3.0 is
# applied only if GitHub is used and the project is public; otherwise there
# is no license. The Gitea server must offer the license.
#PROJECT_LICENSE=AGPL-3.0
# Optional. OWNER/NAME of the SQA-QC-Framework repository on the Gitea server; # Optional. OWNER/NAME of the SQA-QC-Framework repository on the Gitea server;
# it is added to the new project as a submodule over SSH. # it is added to the new project as a submodule over SSH.
# Default: TirSystem/SQA-QC-Framework. # Default: TirSystem/SQA-QC-Framework.
+8 -7
View File
@@ -14,16 +14,17 @@ document of a type. `Primary File` may contain a glob (e.g.
| BC | Business Case | docs/business-case.md | 002 | | BC | Business Case | docs/business-case.md | 002 |
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 005 | | MIL | Milestone / Gateway | docs/milestones/*.md | 008 |
| US | User Story | docs/user-stories.md | 002 | | US | User Story | docs/user-stories.md | 002 |
| UC | Use Case | docs/uc-*/uc.md | 002 | | UC | Use Case | docs/uc-*/uc.md | 003 |
| SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 | | SSD | System Sequence Diagram | docs/uc-*/ssd.md | 003 |
| OC | Operation Contract | docs/uc-*/oc.md | 002 | | OC | Operation Contract | docs/uc-*/oc.md | 003 |
| SD | Sequence Diagram | docs/uc-*/sd.md | 002 | | SD | Sequence Diagram | docs/uc-*/sd.md | 003 |
| DM | Domain Model | docs/domain-model.md | 003 | | DM | Domain Model | docs/domain-model.md | 004 |
| DCD | Design Class Diagram | docs/dcd.md | 004 |
| DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 | | DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 020 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 030 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
## Languages ## Languages
+22 -9
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited UCD-001<br>Justified the qualitative cost-benefit; stakeholder roles replaced by interests; success criteria 2 and 3 reworded for optional GitHub<br>Added objective 7 (documentation) and its success criterion | [02875ae] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added objective 11 (global command, project created in the current folder), a scope item and success criterion 11 | [1cd27f7] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Added objective 8 (project details preset in config.env), the matching scope item and success criterion 8 | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Objective 11, scope item and criterion 11: the configuration files default to the working folder's, then the checkout's | [0ab5006] |
--- ---
@@ -35,13 +35,16 @@ One repeatable, reviewed procedure gives every new project the same secure basel
## Objectives ## Objectives
1. Optionally create an empty GitHub repository under a chosen user or organization. 1. Optionally create an empty GitHub repository under a chosen user or organization.
2. Create a Gitea repository under a chosen user or organization, empty, or with the AGPL license when GitHub is chosen. 2. Create a Gitea repository under a chosen user or organization, empty, or with a license: the one set in `config.env` (`PROJECT_LICENSE`), or AGPL-3.0 when GitHub is chosen, the project is public and none is set.
3. When GitHub was chosen, configure the Gitea repository as a push mirror to GitHub (direction Gitea to GitHub). 3. When GitHub was chosen, configure the Gitea repository as a push mirror to GitHub (direction Gitea to GitHub).
4. Create the local project directory with an `origin` (Gitea) remote and, when GitHub was chosen, a `github` remote, neither containing credentials. 4. Create the local project directory with an `origin` (Gitea) remote and, when GitHub was chosen, a `github` remote, neither containing credentials.
5. Add the SQA-QC-Framework as the `framework` submodule, install its skills and git hooks, and copy its templates, optionally enabling the plan gate. 5. Add the SQA-QC-Framework as the `framework` submodule with its own submodules (the `qc` checklists) fetched, install its skills and git hooks, and copy its templates, optionally enabling the plan gate.
6. Never print or persist a token, and never overwrite existing files or directories without consent. 6. Never print a token or put one in a URL, a remote or a log, write one to disk only in the new project's own `.env` and only after the Maintainer agrees, and never overwrite existing files or directories without consent.
7. Document installation, configuration, usage, security decisions and error handling in clear English for GitHub readers. 7. Document installation, configuration, usage, security decisions and error handling in clear English for GitHub readers.
8. Let the Maintainer preset the project details in `config.env`, so that a detail that is set there is not asked again. 8. Let the Maintainer preset the project details in `config.env`, so that a detail that is set there is not asked again.
9. Ask for a credential that is not provided in `.env` (`GITEA_TOKEN`, `GITHUB_PAT`, `GITHUB_USER`) and, when the Maintainer agrees, create a `.env` file with the credentials the new project needs.
10. Let the Maintainer set the project's license in `config.env` (`PROJECT_LICENSE`), independent of the GitHub choice, or set `none` for no license.
11. Let the Maintainer start the script by name from the folder where the project is to be created, through a command link in a folder on `PATH`, using the `config.env` and `.env` in that folder, or the checkout's when it has none.
## Scope ## Scope
@@ -49,10 +52,14 @@ One repeatable, reviewed procedure gives every new project the same secure basel
- `create-project.sh`, `config.env.example`, `.env.example`, `.gitignore` and `README.md`. - `create-project.sh`, `config.env.example`, `.env.example`, `.gitignore` and `README.md`.
- Safe parsing and validation of `config.env` and `.env` (never `source`d). - Safe parsing and validation of `config.env` and `.env` (never `source`d).
- Prompts for name, description, visibility and owner on each chosen host, and whether to use GitHub (which also applies the AGPL license). Each of these details may be set in `config.env` instead and is then not asked. - Prompts for name, description, visibility and owner on each chosen host, and whether to use GitHub (which also applies the AGPL license when the project is public). Each of these details may be set in `config.env` instead and is then not asked.
- Checks for required tools (`git`, `curl`, optional `jq`) before any change. - Checks for required tools (`git`, `curl`, optional `jq`) before any change.
- A check that the project name is not already taken on GitHub. - A check that the project name is not already taken on GitHub.
- Asking for a credential that `.env` does not provide, and creating the new project's own `.env` (owner-only, ignored by git, never overwritten without a yes).
- A project license set in `config.env` (`PROJECT_LICENSE`, optional, never asked), checked against the licenses the Gitea server offers.
- Partial-failure reporting with a documented way to continue. - Partial-failure reporting with a documented way to continue.
- Starting through a command link: the script finds its own files from the link, the new project lands in the folder it was started in, and `./config.env` and `./.env` there are read before the checkout's (confirmed before the first request).
- Fetching the framework's own submodules (`git submodule update --init --recursive`), so the `qc` checklists are present.
- Documentation of the SSH prerequisite for the submodule (Gitea SSH on port `10022`). - Documentation of the SSH prerequisite for the submodule (Gitea SSH on port `10022`).
### Out of Scope ### Out of Scope
@@ -61,6 +68,7 @@ One repeatable, reviewed procedure gives every new project the same secure basel
- Managing repositories after creation (branch protection, webhooks, teams, CI). - Managing repositories after creation (branch protection, webhooks, teams, CI).
- Hosts other than GitHub and the configured Gitea instance. - Hosts other than GitHub and the configured Gitea instance.
- Creating or rotating tokens and SSH keys. - Creating or rotating tokens and SSH keys.
- Storing a credential anywhere but the new project's `.env` (no password manager, keychain or encryption).
- Making the first commit or opening a pull request. - Making the first commit or opening a pull request.
## Expected Benefits ## Expected Benefits
@@ -83,7 +91,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
| # | Criterion | Target | Measure | | # | Criterion | Target | Measure |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 1 | Credential exposure | 0 occurrences of a token in output, saved remote URLs, config files or leftover temp files | Test run with log review; `git config --get-regexp remote` inspected | | 1 | Credential exposure | 0 occurrences of a token in output, saved remote URLs, tracked files, config files or leftover temp files; a token is written only to the new project's `.env` (owner-only, ignored by git) and only after a yes | Test run with log review; `git config --get-regexp remote` inspected; every file of the new project searched for the tokens |
| 2 | Repository ownership | Each repository created is under the owner chosen at the prompt for that host, never silently under `GITHUB_USER` | Test run with a user owner and with an organization owner | | 2 | Repository ownership | Each repository created is under the owner chosen at the prompt for that host, never silently under `GITHUB_USER` | Test run with a user owner and with an organization owner |
| 3 | Mirror direction | When GitHub is chosen, Gitea is the source and GitHub the target; a push to `origin` appears on GitHub | Push a test commit and compare | | 3 | Mirror direction | When GitHub is chosen, Gitea is the source and GitHub the target; a push to `origin` appears on GitHub | Push a test commit and compare |
| 4 | Partial failure | When one host fails, the output lists what was created and the command to continue | Forced failure test (invalid token for one host) | | 4 | Partial failure | When one host fails, the output lists what was created and the command to continue | Forced failure test (invalid token for one host) |
@@ -91,6 +99,9 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
| 6 | Lint | `shellcheck` reports no errors on `create-project.sh` | `shellcheck create-project.sh` | | 6 | Lint | `shellcheck` reports no errors on `create-project.sh` | `shellcheck create-project.sh` |
| 7 | Documentation | `README.md` covers installation, configuration, usage, security decisions, error handling and stakeholders | Review by S02 against MIL-003 Go/No-Go criterion 6 | | 7 | Documentation | `README.md` covers installation, configuration, usage, security decisions, error handling and stakeholders | Review by S02 against MIL-003 Go/No-Go criterion 6 |
| 8 | Preset details | A project detail set in `config.env` is never asked; an invalid one stops the run before any request and names the key | Tests with each key set, absent, empty and invalid | | 8 | Preset details | A project detail set in `config.env` is never asked; an invalid one stops the run before any request and names the key | Tests with each key set, absent, empty and invalid |
| 9 | Credentials asked and kept | A credential missing from `.env` is asked (not echoed) instead of stopping the run; the new project's `.env` is created only after a yes, owner-only, ignored by git, holding only the keys the project needs, and an existing `.env` is never replaced without a yes | Tests: each credential present and missing, `.env` written, declined, existing, file mode, git exclusion, no token in output |
| 10 | Project license | `PROJECT_LICENSE` set: that license is on the Gitea repository with and without GitHub; `none`: no license; absent: AGPL-3.0 only when GitHub is chosen and the project is public; a license the server does not offer stops the run before anything is created | Tests with a license set, `none`, absent and not offered |
| 11 | Global command | Started through a command link in a `PATH` folder from another folder, the script runs, reads `./config.env` and `./.env` of that folder, else the checkout's, names them before any request, and creates the project under that folder | Test run through a link with files in the folder, in the checkout and in neither; the README example run once |
## Risks ## Risks
@@ -100,6 +111,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
| GitHub PAT lacks permission to create repositories or to push | Creation or mirroring fails | Document the required scopes; check with a read-only API call first and stop with a clear message | | GitHub PAT lacks permission to create repositories or to push | Creation or mirroring fails | Document the required scopes; check with a read-only API call first and stop with a clear message |
| Gitea stores the mirror credentials server-side | A Gitea admin could access the GitHub token | Document it; recommend a fine-grained PAT limited to the one repository where possible | | Gitea stores the mirror credentials server-side | A Gitea admin could access the GitHub token | Document it; recommend a fine-grained PAT limited to the one repository where possible |
| SSH to Gitea port `10022` is not configured | Submodule add fails after repositories already exist | Check SSH reachability before creating anything; document the prerequisite | | SSH to Gitea port `10022` is not configured | Submodule add fails after repositories already exist | Check SSH reachability before creating anything; document the prerequisite |
| A token written to the new project's `.env` is plain text on disk and could be committed or copied by mistake | A leaked token gives access to the hosts | Ask first (default no), write only the keys the project needs, mode owner-only, exclude the file from git through `.git/info/exclude`, never overwrite an existing `.env` without a yes, never print the value, document the risk |
| Framework hook installer changes `core.hooksPath` | An existing hook setup is silently replaced | Inspect the current value first and ask for consent | | Framework hook installer changes `core.hooksPath` | An existing hook setup is silently replaced | Inspect the current value first and ask for consent |
| Repository name conflicts on a host | Creation fails midway | Check availability on both hosts before creating either | | Repository name conflicts on a host | Creation fails midway | Check availability on both hosts before creating either |
@@ -114,6 +126,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
- Bash only, with `git` and `curl` required and `jq` optional. - Bash only, with `git` and `curl` required and `jq` optional.
- `config.env` and `.env` are parsed, never `source`d. - `config.env` and `.env` are parsed, never `source`d.
- No `rm -rf`, and no token in any URL, log or remote. - No `rm -rf`, and no token in any URL, log or remote.
- A token on disk only in the new project's `.env`, created by the script with the Maintainer's consent.
- The framework under `framework/` is not edited from this project. - The framework under `framework/` is not edited from this project.
## Cost–Benefit Assessment ## Cost–Benefit Assessment
@@ -140,5 +153,5 @@ Proceed — the procedure is small, well bounded and removes a repeated, securit
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[UCD-001]: ./use-case-diagram.md [UCD-001]: ./use-case-diagram.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+377
View File
@@ -0,0 +1,377 @@
# Design Class Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | DCD-002 |
| CrossReference | [DCD-001], [DCD-003], [DM-002], [UC-001], [UC-002], [OC-001], [SD-001], [SD-002], [DICT-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added Launcher, Checkout and WorkingFolder; startProjectCreation takes the checkout and working folder (from DCD-003, UC-002) | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Default configuration files: --config and --env, else ./config.env and ./.env in the working folder, else the checkout's | [0ab5006] |
---
## Purpose and Scope
The consolidated design model of the project. Use-case models are scoped views; when one changes, this model is checked and updated in the same change. It currently covers [UC-001] "Create a new project" ([DCD-001]), which it was created from, and refines the concepts of [DM-002]. Class and attribute names are the IT terms of [DICT-001]. Every method traces to a contract in [OC-001] or a message in [SD-001].
The classes are design classes of a Bash program: a class is a group of functions in `src/lib/` with its data held in the shared state arrays (see "Implementation Mapping").
## Diagram
```plantuml
@startuml
skinparam classAttributeIconSize 0
hide empty members
enum Visibility {
private
public
}
class ProjectCreator <<controller>> {
+startProjectCreation(workingFolder : WorkingFolder, configFiles : ConfigFiles) : PromptSet
+provideProjectDetails(name : String, description : String, visibility : Visibility, giteaOwner : Owner, githubOwner : Owner [0..1], directory : Path, enablePlanGate : Boolean, writeEnvFile : Boolean) : Summary
}
class ConfigLoader {
+load(configFile : Path, envFile : Path) : Configuration
}
class CredentialCollector {
+collect(configuration : Configuration, kinds : String [1..3]) : Configuration
}
class EnvFileWriter {
+write(project : LocalProject, configuration : Configuration, hasGithub : Boolean) : EnvFile [0..1]
}
class ToolChecker {
+check(tools : String [1..*]) : ToolCheck
}
class Preflight {
+check(request : ProjectRequest) : PreflightResult
}
abstract class GitHost <<facade>> {
-name : String
-webAddress : String
-apiAddress : String
+verifyToken() : Boolean
+ownerAccepts(owner : Owner) : Boolean
+nameFree(name : String) : Boolean
}
class GiteaClient <<facade>> {
+GiteaClient(configuration : Configuration)
+hasLicense(key : String) : Boolean
+createRepository(request : ProjectRequest, license : String [0..1]) : GiteaRepository
+addPushMirror(source : GiteaRepository, target : GitHubRepository) : PushMirror
-requestSync(mirror : PushMirror) : void
}
class GitHubClient <<facade>> {
+GitHubClient(configuration : Configuration)
+createEmptyRepository(request : ProjectRequest) : GitHubRepository
}
class LocalProjectBuilder {
+build(directory : Path, source : GiteaRepository, target : GitHubRepository [0..1], sshPassed : Boolean) : LocalProject
}
class FrameworkInstaller {
+install(project : LocalProject, enablePlanGate : Boolean) : InstallResult
}
class SummaryReport {
+compose(request : ProjectRequest) : Summary
}
class Launcher {
+resolveCheckout(invocation : Path) : Checkout
+currentFolder() : WorkingFolder
+locateConfigFiles(configPath : Path [0..1], envPath : Path [0..1], checkout : Checkout, workingFolder : WorkingFolder) : ConfigFiles
+startFromWorkingFolder(configPath : Path [0..1], envPath : Path [0..1]) : PromptSet
}
class Checkout {
-path : Path
+configFile() : Path
+envFile() : Path
}
class WorkingFolder {
-path : Path
+configFile() : Path [0..1]
+envFile() : Path [0..1]
}
class ConfigFiles {
-configFile : Path
-envFile : Path
}
class Run {
-isApply : Boolean
}
class Configuration {
-giteaUrl : String
-giteaApiUrl : String
-githubWebUrl : String
-githubApiUrl : String
-giteaSshPort : Integer
-mirrorInterval : String
-frameworkRepo : String
-presetDetails : Map [0..1]
}
class Credential {
-kind : String
-value : String
}
class ToolCheck {
-hasGit : Boolean
-hasCurl : Boolean
-hasJq : Boolean
}
class PromptSet {
-prompts : String [1..*]
}
class ProjectRequest {
-name : String
-description : String
-visibility : Visibility
-directory : Path
-enablePlanGate : Boolean
-license : String [0..1]
}
class Owner {
-name : String
-kind : String
}
class PreflightResult {
-tokensWork : Boolean
-ownersAccept : Boolean
-nameIsFree : Boolean
-licenseIsOffered : Boolean
-sshPassed : Boolean
}
abstract class Repository {
-name : String
-description : String
-visibility : Visibility
-address : String
}
class GiteaRepository
class GitHubRepository
class LicenseFile {
-key : String
}
class PushMirror {
-interval : String
-syncOnCommit : Boolean
}
class LocalProject {
-directory : Path
}
class Remote {
-name : String
-address : String
}
class Submodule {
-name : String
-address : String
}
class HookSetup {
-areSkillsInstalled : Boolean
-areHooksInstalled : Boolean
-isPlanGateEnabled : Boolean
}
class EnvFile {
-address : Path
-keys : String [1..3]
}
class Template {
-name : String
-isCopied : Boolean
}
class InstallResult <<dto>>
class Summary {
-createdItems : String [0..*]
-skippedItems : String [0..*]
-nextSteps : String [0..*]
}
Launcher "1" --> "1" ProjectCreator : starts
Launcher ..> Checkout : creates
Launcher ..> WorkingFolder : creates
Launcher ..> ConfigFiles : creates
Run "1" *-- "1" ConfigFiles
Run "1" *-- "1" Checkout
Run "1" *-- "1" WorkingFolder
ProjectCreator ..> ConfigLoader : creates
ProjectCreator ..> ToolChecker : creates
ProjectCreator ..> CredentialCollector : creates
ProjectCreator ..> EnvFileWriter : creates [0..1]
ProjectCreator ..> Preflight : creates
ProjectCreator ..> GiteaClient : creates
ProjectCreator ..> GitHubClient : creates [0..1]
ProjectCreator ..> LocalProjectBuilder : creates
ProjectCreator ..> FrameworkInstaller : creates
ProjectCreator ..> SummaryReport : creates
Preflight ..> GiteaClient : asks
Preflight ..> GitHubClient : asks [0..1]
GitHost <|-- GiteaClient
GitHost <|-- GitHubClient
GitHost "1" --> "0..*" Owner : has
GiteaClient ..> Configuration
GitHubClient ..> Configuration
ProjectCreator "0..*" --> "1" Run
Run "1" *-- "1" Configuration
Run "1" *-- "1" ToolCheck
Run "1" *-- "0..1" ProjectRequest
Run "1" --> "1" PromptSet : returns
Configuration "1" *-- "1..3" Credential
ProjectRequest "0..*" --> "1" Owner : giteaOwner
ProjectRequest "0..*" --> "0..1" Owner : githubOwner
ProjectRequest "1" *-- "0..1" PreflightResult
ProjectRequest "1" --> "0..1" GiteaRepository : stored in
ProjectRequest "1" --> "0..1" GitHubRepository : also stored in
ProjectRequest "1" --> "0..1" LocalProject : working copy
Summary "0..*" --> "1" ProjectRequest : reports on
Repository <|-- GiteaRepository
Repository <|-- GitHubRepository
Repository "0..*" --> "1" Owner : owned by
GiteaRepository "1" *-- "0..1" LicenseFile
PushMirror "0..*" --> "1" GiteaRepository : source
PushMirror "0..*" --> "1" GitHubRepository : target
PushMirror "0..*" --> "1" Credential : authorised by
LocalProject "1" *-- "1..2" Remote
Remote "0..*" --> "1" Repository : points to
LocalProject "1" *-- "1" Submodule
LocalProject "1" *-- "1" HookSetup
LocalProject "1" *-- "0..*" Template
LocalProject "1" *-- "0..1" EnvFile
EnvFile "0..*" --> "1..3" Credential : copy of
InstallResult "0..*" --> "1" Submodule
InstallResult "0..*" --> "1" HookSetup
InstallResult "0..*" --> "0..*" Template
ProjectRequest "0..*" --> "1" Visibility
Repository "0..*" --> "1" Visibility
@enduml
```
## Class Table
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
| --- | --- | --- | --- | --- |
| `Launcher` | Command Link (the object that follows it) | Follows the command link to the checkout, takes the folder the Maintainer stands in, chooses the two configuration files, and starts the run. | none | `resolveCheckout`, `currentFolder`, `locateConfigFiles`, `startFromWorkingFolder` |
| `Checkout` | Checkout | Names the folder that holds the script's own files and the default `config.env` and `.env`. | `path` | `configFile`, `envFile` |
| `WorkingFolder` | Working Folder | Names the base of the default directory of the new project and the files it may hold. | `path` | `configFile`, `envFile` |
| `ConfigFiles` | none (system concept of [OC-002]) | Carries the two files chosen for the `Configuration`. | `configFile`, `envFile` | none |
| `ProjectCreator` | none (controller for the system operations of [OC-001]) | Receives the two system operations, sequences the steps and stops on the first failure. | none | `startProjectCreation`, `provideProjectDetails` |
| `ConfigLoader` | Configuration | Reads `config.env` and `.env` as plain text and validates every value, preset project details included. | none | `load` |
| `CredentialCollector` | none (system concept) | Asks, without echo, for a credential that `.env` does not provide and validates it like one read from `.env`. | none | `collect` |
| `EnvFileWriter` | Credentials File | Creates the project's own `.env` with the credentials the project needs: owner-only, excluded from git, never replaced without a yes. | none | `write` |
| `ToolChecker` | none (system concept `ToolCheck`) | Detects the required and optional tools. | none | `check` |
| `Preflight` | none (system concept `PreflightResult`) | Runs the read-only checks of both hosts before anything is created. | none | `check` |
| `GitHost` | Git Host | The operations every host offers: check the token, check that an owner accepts new repositories, check that a name is free. | `name`, `webAddress`, `apiAddress` | `verifyToken`, `ownerAccepts`, `nameFree` |
| `GiteaClient` | Git Host (Gitea) | Hides the Gitea API and its token; creates the repository and the push mirror. | none beyond `GitHost` (uses `Configuration`) | `GiteaClient`, `hasLicense`, `createRepository`, `addPushMirror`, `requestSync` |
| `GitHubClient` | Git Host (GitHub) | Hides the GitHub API and its token; creates the empty repository. | none beyond `GitHost` (uses `Configuration`) | `GitHubClient`, `createEmptyRepository` |
| `LocalProjectBuilder` | Local Project, Remote | Creates the project directory, its git repository and its credential-free remotes. | none | `build` |
| `FrameworkInstaller` | Framework, Framework Setup, Template | Adds the framework submodule, installs skills and hooks once, and copies the templates without overwriting. | none | `install` |
| `SummaryReport` | Summary | Composes the report of what was created, skipped or failed. | none | `compose` |
| `Run` | none (system concept) | Holds the state of one execution. | `isApply` | none |
| `Configuration` | Configuration | Holds the service addresses, the credentials and any preset project details. | `giteaUrl`, `giteaApiUrl`, `githubWebUrl`, `githubApiUrl`, `giteaSshPort`, `mirrorInterval`, `frameworkRepo`, `presetDetails` | none |
| `Credential` | Access Token | Holds a secret in memory only; it never becomes part of an address or a message. | `kind`, `value` | none |
| `ToolCheck` | none (system concept) | Records which tools are present. | `hasGit`, `hasCurl`, `hasJq` | none |
| `PromptSet` | none (system concept) | The questions still to ask; a detail preset in `config.env` is not in it. | `prompts` | none |
| `ProjectRequest` | Project | Holds the details of the project being created. | `name`, `description`, `visibility`, `directory`, `enablePlanGate`, `license` | none |
| `Owner` | Owner | A user or organization on a host. | `name`, `kind` | none |
| `PreflightResult` | none (system concept) | Records the outcome of the preflight checks. | `tokensWork`, `ownersAccept`, `nameIsFree`, `licenseIsOffered`, `sshPassed` | none |
| `Repository` | Repository | Common data of a repository on a host. | `name`, `description`, `visibility`, `address` | none |
| `GiteaRepository` | Gitea Repository | The source of truth. | none beyond `Repository` | none |
| `GitHubRepository` | GitHub Repository | Receives its content from the mirror. | none beyond `Repository` | none |
| `LicenseFile` | License | The license file in the Gitea repository when a license applies. | `key` | none |
| `PushMirror` | Mirror | The Gitea to GitHub push mirror. | `interval`, `syncOnCommit` | none |
| `LocalProject` | Local Project | The project directory on the Maintainer's machine. | `directory` | none |
| `Remote` | Remote | A named link to a repository (`origin`, `github`), without a credential. | `name`, `address` | none |
| `Submodule` | Framework | The framework added to the local project. | `name`, `address` | none |
| `HookSetup` | Framework Setup | Records the skills and hooks installed and the plan gate state. | `areSkillsInstalled`, `areHooksInstalled`, `isPlanGateEnabled` | none |
| `EnvFile` | Credentials File | The `.env` of the project: a copy of the credentials it needs. | `address`, `keys` | none |
| `Template` | Template | A framework file copied into the project. | `name`, `isCopied` | none |
| `InstallResult` | none (carries the result of one operation) | Returns the submodule, the hook setup and the templates of `install`. | none | none |
| `Summary` | Summary | The report returned to the Maintainer; it contains no credential. | `createdItems`, `skippedItems`, `nextSteps` | none |
| `Visibility` | none (enumeration of a Project and Repository attribute) | The two allowed visibilities. | `private`, `public` | none |
## Method Traceability
| Method signature | Operation Contract / SD message |
| --- | --- |
| `ProjectCreator.startProjectCreation(workingFolder, configFiles) : PromptSet` | [OC-001] `startProjectCreation`; [SD-001] `startProjectCreation()`; [SD-002] `startProjectCreation(checkout, workingFolder, ...)` |
| `Launcher.startFromWorkingFolder(configPath, envPath) : PromptSet` | [OC-002] `startFromWorkingFolder`; [SD-002] |
| `Launcher.resolveCheckout(invocation) : Checkout`, `Launcher.currentFolder() : WorkingFolder` | [OC-002] P2, P3; [SD-002] |
| `Launcher.locateConfigFiles(configPath, envPath, checkout, workingFolder) : ConfigFiles`, `WorkingFolder.configFile()`, `WorkingFolder.envFile()` | [OC-002] P5; [SD-002] `locateConfigFiles(...)` |
| `ProjectCreator.provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile) : Summary` | [OC-001] `provideProjectDetails`; [SD-001] `provideProjectDetails(...)` |
| `ConfigLoader.load(configFile, envFile) : Configuration` | [SD-001] `load(config.env, .env)`; [OC-001] `startProjectCreation` P2 |
| `CredentialCollector.collect(configuration, kinds) : Configuration` | [SD-001] `collect(configuration, GITEA_TOKEN)` and `collect(configuration, GITHUB_PAT, GITHUB_USER)`; [OC-001] `startProjectCreation` P2 and the precondition of `provideProjectDetails` |
| `EnvFileWriter.write(project, configuration, hasGithub) : EnvFile` | [SD-001] `write(localProject, configuration, githubOwner present)`; [OC-001] `provideProjectDetails` P14 |
| `ToolChecker.check(tools) : ToolCheck` | [SD-001] `check(git, curl, jq)`; [OC-001] `startProjectCreation` P3 |
| `Preflight.check(request) : PreflightResult` | [SD-001] `check(request)`; [OC-001] `provideProjectDetails` P2 |
| `GiteaClient(configuration)` | [SD-001] `new(configuration)` to `GiteaClient` |
| `GitHost.verifyToken() : Boolean` | [SD-001] `verifyToken()` from `Preflight` to either client; P2 |
| `GitHost.ownerAccepts(owner) : Boolean` | [SD-001] `ownerAccepts(giteaOwner)` and `ownerAccepts(githubOwner)`; P2 |
| `GitHost.nameFree(name) : Boolean` | [SD-001] `nameFree(name)` to either client; P2 |
| `GiteaClient.hasLicense(key) : Boolean` | [SD-001] `hasLicense(license)`; P2 |
| `GiteaClient.createRepository(request, license) : GiteaRepository` | [SD-001] `createRepository(request, license)`; P3, P4 |
| `GiteaClient.addPushMirror(source, target) : PushMirror` | [SD-001] `addPushMirror(giteaRepository, gitHubRepository)`; P6 |
| `GiteaClient.requestSync(mirror) : void` | [SD-001] `requestSync(pushMirror)`; P6 |
| `GitHubClient(configuration)` | [SD-001] `new(configuration)` to `GitHubClient` |
| `GitHubClient.createEmptyRepository(request) : GitHubRepository` | [SD-001] `createEmptyRepository(request)`; P5 |
| `LocalProjectBuilder.build(directory, source, target, sshPassed) : LocalProject` | [SD-001] `build(directory, giteaRepository, gitHubRepository, sshPassed)`; P7, P8, P9 |
| `FrameworkInstaller.install(project, enablePlanGate) : InstallResult` | [SD-001] `install(localProject, enablePlanGate)`; P10, P11, P12 |
| `SummaryReport.compose(request) : Summary` | [SD-001] `compose(projectRequest)`; P13 |
## Pattern Annotations
| Pattern | Classes | Rationale |
| --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | One entry for the system operations; coordinates and does no HTTP, git or file work itself |
| Facade (GoF) | `GitHost`, `GiteaClient`, `GitHubClient` | Each client hides one host's HTTP API and keeps the token inside; no other class sees a credential. `GitHost` holds the operations both share |
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker`, `CredentialCollector`, `EnvFileWriter`, `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport` | No domain concept owns these responsibilities; small units keep cohesion high |
| Creator (GRASP) | `ConfigLoader` creates `Configuration`; `GiteaClient` creates `GiteaRepository` and `PushMirror` | The creating class holds the data needed to build the object |
| Protection from variations (GRASP) | `GiteaClient`, `GitHubClient`, `ProjectRequest` | The optional GitHub path is decided by the controller; the clients do not know it |
| Data Transfer Object (GoF-style) | `InstallResult` | Carries the three results of `install` in one return value |
## Dependency Check
No circular dependency. `ProjectCreator` depends on every helper class and no helper depends on it. `Preflight` depends on the two clients; the clients extend `GitHost` and depend only on `Configuration`. The data classes form a tree: `Run` holds `Configuration`, `ToolCheck` and `ProjectRequest`; `ProjectRequest` reaches the repositories and the `LocalProject`; `Summary` points at `ProjectRequest` and nothing points back at it. `CredentialCollector` and `EnvFileWriter` depend only on `Configuration`, `Credential` and `LocalProject`; the only class that holds a secret after the run is `EnvFile`, and only as a copy written to the Maintainer's own disk. `Repository` is shared by `Remote` and `PushMirror` without a cycle.
SOLID check: no class has more than one reason to change (one host API, one kind of local work, one report); the clients can be replaced behind the same operations; the controller depends on the operations, not on how a host or git is called. `ProjectCreator` has two operations and no data, so it is not a god class.
## Implementation Mapping
| Design class | Where it lives in `src/` |
| --- | --- |
| `Launcher` | `create-project.sh` (the start of `main`: where the script finds its own folder) |
| `ProjectCreator` | `create-project.sh` (`main`), `lib/apply.sh` |
| `ConfigLoader` | `lib/config.sh` (`load_configuration`), `lib/validate.sh` |
| `ToolChecker` | `lib/tools.sh` |
| `CredentialCollector` | planned for [MIL-005]: `lib/credentials.sh`, with `lib/prompts.sh` |
| `EnvFileWriter` | planned for [MIL-005]: `lib/envfile.sh` |
| `Preflight` | `lib/preflight.sh` |
| `GitHost`, `GiteaClient`, `GitHubClient` | `lib/api.sh`, `lib/http.sh`, `lib/json.sh`, `lib/repositories.sh`, `lib/mirror.sh`, `lib/hosts.sh` |
| `LocalProjectBuilder` | `lib/localproject.sh`, `lib/git.sh` |
| `FrameworkInstaller` | `lib/framework.sh` |
| `SummaryReport` | `lib/steps.sh`, `lib/plan.sh` |
| `PromptSet`, `ProjectRequest` | `lib/project.sh`, `lib/prompts.sh` |
| `Run`, `Configuration`, `Credential`, `PreflightResult` | the state arrays declared in `lib/constants.sh` |
---
[DCD-001]: ./uc-001/dcd.md
[DCD-003]: ./uc-002/dcd.md
[UC-002]: ./uc-002/uc.md
[SD-002]: ./uc-002/sd.md
[OC-002]: ./uc-002/oc.md
[DM-002]: ./domain-model.md
[UC-001]: ./uc-001/uc.md
[OC-001]: ./uc-001/oc.md
[SD-001]: ./uc-001/sd.md
[MIL-005]: ./milestones/mil-005-credentials.md
[DICT-001]: ./dictionary.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+35 -22
View File
@@ -4,12 +4,13 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | DICT-001 | | ID | DICT-001 |
| CrossReference | [BC-001], [SA-001], [DM-001], [DM-002] | | CrossReference | [BC-001], [SA-001], [DM-001], [DM-002], [DM-003] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version, terms of UC-001 | [02875ae] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added Command Link, Checkout and Working Folder (DM-003, UC-002) | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | `ConfigFiles` named as a system concept without a PO term | [0ab5006] |
--- ---
@@ -21,39 +22,51 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
| PO term | Language | IT term | Definition | Used as PO term in | Used as IT term in | | PO term | Language | IT term | Definition | Used as PO term in | Used as IT term in |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| Maintainer | en | Maintainer | The person who creates a new project. | DM, UC, US | OC, SD | | Maintainer | en | Maintainer | The person who creates a new project. | DM, UC, US | OC, SD, DCD |
| Project | en | ProjectRequest | The new software project being set up, with its name, description and visibility. | DM, UC, US | OC, SD | | Project | en | ProjectRequest | The new software project being set up, with its name, description and visibility. | DM, UC, US | OC, SD, DCD |
| Configuration | en | Configuration | The service addresses and access tokens set up before starting. | DM, UC | OC, SD | | Configuration | en | Configuration | The service addresses and access tokens set up before starting. | DM, UC | OC, SD, DCD |
| Git Host | en | GitHost | A service that holds repositories: Gitea or GitHub. | DM, UC | OC, SD | | Git Host | en | GitHost | A service that holds repositories: Gitea or GitHub. | DM, UC | OC, SD, DCD |
| Access Token | en | Credential | A secret that lets the Maintainer act on a Git Host; never part of an address. | DM, UC | OC, SD | | Access Token | en | Credential | A secret that lets the Maintainer act on a Git Host; never part of an address. | DM, UC | OC, SD, DCD |
| Owner | en | Owner | The user or organization on a Git Host that owns repositories. | DM, UC | OC, SD | | Owner | en | Owner | The user or organization on a Git Host that owns repositories. | DM, UC | OC, SD, DCD |
| Repository | en | Repository | A place on a Git Host that holds a project's history. | DM, UC | OC, SD | | Repository | en | Repository | A place on a Git Host that holds a project's history. | DM, UC | OC, SD, DCD |
| Gitea Repository | en | GiteaRepository | The repository on Gitea; the source of truth. | DM, UC | OC, SD | | Gitea Repository | en | GiteaRepository | The repository on Gitea; the source of truth. | DM, UC | OC, SD, DCD |
| GitHub Repository | en | GitHubRepository | The repository on GitHub; it receives its content from the mirror. | DM, UC | OC, SD | | GitHub Repository | en | GitHubRepository | The repository on GitHub; it receives its content from the mirror. | DM, UC | OC, SD, DCD |
| License | en | LicenseFile | The legal terms file (AGPL-3.0) added to the Gitea repository when GitHub is chosen. | DM, UC | OC, SD | | License | en | LicenseFile | The legal terms file added to the Gitea repository when a license applies. | DM, UC | OC, SD, DCD |
| Mirror | en | PushMirror | The push mirror that copies a Gitea repository to a GitHub repository. | DM, UC | OC, SD | | Mirror | en | PushMirror | The push mirror that copies a Gitea repository to a GitHub repository. | DM, UC | OC, SD, DCD |
| Local Project | en | LocalProject | The project directory on the Maintainer's machine. | DM, UC | OC, SD | | Local Project | en | LocalProject | The project directory on the Maintainer's machine. | DM, UC | OC, SD, DCD |
| Remote | en | Remote | A named link from a local project to a repository. | DM, UC | OC, SD | | Remote | en | Remote | A named link from a local project to a repository. | DM, UC | OC, SD, DCD |
| Framework | en | Submodule | The SQA-QC-Framework added to a local project; the IT term names how it is attached. | DM, UC | OC, SD | | Framework | en | Submodule | The SQA-QC-Framework added to a local project; the IT term names how it is attached. | DM, UC | OC, SD, DCD |
| Framework Setup | en | HookSetup | The skills and git hooks installed from the framework, with the plan gate on or off. | DM, UC | OC, SD | | Framework Setup | en | HookSetup | The skills and git hooks installed from the framework, with the plan gate on or off. | DM, UC | OC, SD, DCD |
| Template | en | Template | A framework file copied into a project. | DM, UC | OC, SD | | Template | en | Template | A framework file copied into a project. | DM, UC | OC, SD, DCD |
| Summary | en | Summary | The report of what was created, skipped or failed and how to continue. | DM, UC | OC, SD | | Credentials File | en | EnvFile | The file in the local project that holds a copy of the credentials the project needs; owner-only and ignored by git. | DM, UC | OC, SD, DCD |
| Command Link | en | CommandLink | A name on the shell's search path that leads to the script; made by the Maintainer, only followed by the system (no design class). | DM, UC | OC, SD, DCD |
| Checkout | en | Checkout | The folder that holds RepoFoundry and its default `config.env` and `.env`. | DM, UC | OC, SD, DCD |
| Working Folder | en | WorkingFolder | The folder the Maintainer starts the script in; the base of the default project directory. | DM, UC | OC, SD, DCD |
| Summary | en | Summary | The report of what was created, skipped or failed and how to continue. | DM, UC | OC, SD, DCD |
## Rules ## Rules
- The Domain Model, use cases and user stories use the PO term; the Operation - The Domain Model, use cases and user stories use the PO term; the Operation
Contract, Sequence Diagram, Design Class Diagram and ERD use the IT term. Contract, Sequence Diagram, Design Class Diagram and ERD use the IT term.
- One IT term per PO term and one PO term per IT term; no synonyms. - One IT term per PO term and one PO term per IT term; no synonyms.
- `Run`, `ToolCheck`, `PreflightResult` and `PromptSet` appear in [OC-001] but - `Run`, `ToolCheck`, `PreflightResult`, `PromptSet` and `ConfigFiles` (the two
have no PO term: they are system concepts, not domain concepts, and are not files chosen for the Configuration, in [OC-002]) appear in the Operation
Contracts but have no PO term: they are system concepts, not domain concepts, and are not
in the Domain Model. in the Domain Model.
- `InstallResult` and the enumeration `Visibility` appear only in [DCD-001]:
`InstallResult` carries the three results of one operation, and `Visibility`
is the type of the Project and Repository attribute of the same name. Neither
is a domain concept.
- A new concept in a Domain Model gets a row here in the same change. - A new concept in a Domain Model gets a row here in the same change.
--- ---
[BC-001]: ./business-case.md [BC-001]: ./business-case.md
[DM-003]: ./uc-002/dm.md
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[DM-001]: ./uc-001/dm.md [DM-001]: ./uc-001/dm.md
[DM-002]: ./domain-model.md [DM-002]: ./domain-model.md
[OC-001]: ./uc-001/oc.md [OC-001]: ./uc-001/oc.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [DCD-001]: ./uc-001/dcd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+42 -7
View File
@@ -9,14 +9,14 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version, created from [DM-001] (UC-001) | [02875ae] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added Command Link, Checkout and Working Folder (from DM-003, UC-002) | [1cd27f7] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Configuration may hold preset project details (from DM-001, UC-001 step 3) | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Default configuration files: --config and --env, else ./config.env and ./.env in the working folder, else the checkout's | [0ab5006] |
--- ---
## Purpose and Scope ## Purpose and Scope
The consolidated model of the project. Use-case models are scoped views; when one changes, this model is checked and updated in the same change. It currently covers [UC-001] "Create a new project" ([DM-001]), which it was created from. Concept names are the PO terms recorded in [DICT-001]. The consolidated model of the project. Use-case models are scoped views; when one changes, this model is checked and updated in the same change. It currently covers [UC-001] "Create a new project" ([DM-001]), which it was created from, and [UC-002] "Start the script as a global command" ([DM-003]). Concept names are the PO terms recorded in [DICT-001].
## Diagram ## Diagram
@@ -79,6 +79,19 @@ class "Framework Setup" as FrameworkSetup {
class Template { class Template {
name name
} }
class "Credentials File" as CredentialsFile {
address
}
class "Command Link" as CommandLink {
name
folder
}
class Checkout {
path
}
class "Working Folder" as WorkingFolder {
path
}
class Summary { class Summary {
created items created items
skipped items skipped items
@@ -108,7 +121,15 @@ LocalProject "1" --> "1" FrameworkSetup : has
FrameworkSetup "0..*" --> "1" Framework : is installed from FrameworkSetup "0..*" --> "1" Framework : is installed from
Framework "1" --> "1..*" Template : provides Framework "1" --> "1..*" Template : provides
LocalProject "1" --> "0..*" Template : contains a copy of LocalProject "1" --> "0..*" Template : contains a copy of
LocalProject "1" --> "0..1" CredentialsFile : has
CredentialsFile "1" --> "1..2" AccessToken : holds a copy of
Summary "1" --> "1" Project : reports on Summary "1" --> "1" Project : reports on
Maintainer "1" --> "0..*" CommandLink : makes
CommandLink "0..*" --> "1" Checkout : leads to
Maintainer "1" --> "1" WorkingFolder : starts the script in
Checkout "1" --> "1" Configuration : holds by default
WorkingFolder "1" --> "0..1" Configuration : may hold
WorkingFolder "1" --> "0..*" LocalProject : is the base of
@enduml @enduml
``` ```
@@ -125,13 +146,17 @@ Summary "1" --> "1" Project : reports on
| Repository | A place on a Git Host that holds a project's history | name, description, visibility, address | [UC-001] steps 5 and 6 "repository" | | Repository | A place on a Git Host that holds a project's history | name, description, visibility, address | [UC-001] steps 5 and 6 "repository" |
| Gitea Repository | The Repository on Gitea; the source of truth | none beyond Repository | [UC-001] step 6 | | Gitea Repository | The Repository on Gitea; the source of truth | none beyond Repository | [UC-001] step 6 |
| GitHub Repository | The Repository on GitHub; receives its content from the Mirror | none beyond Repository | [UC-001] step 5 | | GitHub Repository | The Repository on GitHub; receives its content from the Mirror | none beyond Repository | [UC-001] step 5 |
| License | The legal terms file added to a Gitea Repository (AGPL-3.0) when GitHub is chosen | name | [UC-001] step 6 "AGPL license" | | License | The legal terms file added to a Gitea Repository when a license applies: the one set in the Configuration, or AGPL-3.0 when GitHub is chosen, the project is public and none is set | name | [UC-001] step 6 "license" |
| Mirror | The push mirror that copies a Gitea Repository to a GitHub Repository | interval, sync on commit | [UC-001] step 7 "push mirror" | | Mirror | The push mirror that copies a Gitea Repository to a GitHub Repository | interval, sync on commit | [UC-001] step 7 "push mirror" |
| Local Project | The project directory on the Maintainer's machine | directory | [UC-001] step 8 "local project" | | Local Project | The project directory on the Maintainer's machine | directory | [UC-001] step 8 "local project" |
| Remote | A named link from a Local Project to a Repository (`origin`, `github`) | name, address | [UC-001] step 8 "remote" | | Remote | A named link from a Local Project to a Repository (`origin`, `github`) | name, address | [UC-001] step 8 "remote" |
| Framework | The SQA-QC-Framework added to a Local Project | name, address | [UC-001] step 9 "framework submodule" | | Framework | The SQA-QC-Framework added to a Local Project | name, address | [UC-001] step 9 "framework submodule" |
| Framework Setup | The skills and git hooks installed from the Framework, with the plan gate on or off | plan gate enabled | [UC-001] step 9 "skills and hooks", "plan gate" | | Framework Setup | The skills and git hooks installed from the Framework, with the plan gate on or off | plan gate enabled | [UC-001] step 9 "skills and hooks", "plan gate" |
| Template | A file the Framework provides to copy into a project (`AGENTS.md`, artifact registry) | name | [UC-001] step 9 "templates" | | Template | A file the Framework provides to copy into a project (`AGENTS.md`, artifact registry) | name | [UC-001] step 9 "templates" |
| Credentials File | The file in a Local Project that holds a copy of the Access Tokens (and the GitHub account name) the project needs; readable by its owner only and ignored by git | address | [UC-001] step 9 "credentials file" |
| Command Link | A name in a folder on the shell's search path that leads to the script in the Checkout | name, folder | [UC-002] step 1 "command link" |
| Checkout | The folder that holds RepoFoundry: the script, its own files and by default `config.env` and `.env` | path | [UC-002] step 4 "checkout" |
| Working Folder | The folder in which the Maintainer starts the script, under which the new project is created by default, and which may hold its own `config.env` and `.env` | path | [UC-002] step 2 "working folder" |
| Summary | The report of what was created, skipped or failed and how to continue | created items, skipped items, next steps | [UC-001] step 10 "summary" | | Summary | The report of what was created, skipped or failed and how to continue | created items, skipped items, next steps | [UC-001] step 10 "summary" |
## Association Table ## Association Table
@@ -146,7 +171,7 @@ Summary "1" --> "1" Project : reports on
| Owner | owns | Repository | 1 to 0..* | | Owner | owns | Repository | 1 to 0..* |
| Project | is stored in | Gitea Repository | 1 to 1 | | Project | is stored in | Gitea Repository | 1 to 1 |
| Project | is also stored in | GitHub Repository | 1 to 0..1 | | Project | is also stored in | GitHub Repository | 1 to 0..1 |
| Gitea Repository | has | License | 1 to 0..1 (1 when GitHub is chosen) | | Gitea Repository | has | License | 1 to 0..1 (1 when a license applies) |
| Mirror | copies from | Gitea Repository | 1 to 1 | | Mirror | copies from | Gitea Repository | 1 to 1 |
| Mirror | copies to | GitHub Repository | 1 to 1 | | Mirror | copies to | GitHub Repository | 1 to 1 |
| Mirror | is authorised by | Access Token | 0..* to 1 | | Mirror | is authorised by | Access Token | 0..* to 1 |
@@ -158,7 +183,15 @@ Summary "1" --> "1" Project : reports on
| Framework Setup | is installed from | Framework | 0..* to 1 | | Framework Setup | is installed from | Framework | 0..* to 1 |
| Framework | provides | Template | 1 to 1..* | | Framework | provides | Template | 1 to 1..* |
| Local Project | contains a copy of | Template | 1 to 0..* | | Local Project | contains a copy of | Template | 1 to 0..* |
| Local Project | has | Credentials File | 1 to 0..1 |
| Credentials File | holds a copy of | Access Token | 1 to 1..2 |
| Summary | reports on | Project | 1 to 1 | | Summary | reports on | Project | 1 to 1 |
| Maintainer | makes | Command Link | 1 to 0..* |
| Command Link | leads to | Checkout | 0..* to 1 |
| Maintainer | starts the script in | Working Folder | 1 to 1 |
| Checkout | holds by default | Configuration | 1 to 1 |
| Working Folder | may hold | Configuration | 1 to 0..1 |
| Working Folder | is the base of | Local Project | 1 to 0..* |
## Generalizations ## Generalizations
@@ -169,8 +202,10 @@ Summary "1" --> "1" Project : reports on
--- ---
[UC-001]: ./uc-001/uc.md [UC-001]: ./uc-001/uc.md
[UC-002]: ./uc-002/uc.md
[DM-003]: ./uc-002/dm.md
[SSD-001]: ./uc-001/ssd.md [SSD-001]: ./uc-001/ssd.md
[DICT-001]: ./dictionary.md [DICT-001]: ./dictionary.md
[DM-001]: ./uc-001/dm.md [DM-001]: ./uc-001/dm.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+82
View File
@@ -0,0 +1,82 @@
# MIL-005 Credentials
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-005 |
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [ded26a6] |
---
## Purpose
Decide whether the script can ask for a credential that `.env` does not provide and create the new project's own `.env`, without exposing a token: asked without echo, written only after a yes, owner-only, ignored by git, never replacing an existing file, and never shown in any output.
## Deliverable
`create-project.sh` that (1) no longer stops when `.env` is missing or lacks a credential but asks for it without echo (`GITEA_TOKEN` always; `GITHUB_PAT` and `GITHUB_USER` once GitHub is chosen), validating each value like one read from `.env`; and (2) after the local project exists, asks whether to create a `.env` in it and, on a yes, writes only the keys the project needs: `GITEA_TOKEN`, and `GITHUB_PAT` and `GITHUB_USER` when GitHub was chosen. The file is readable by its owner only, excluded from git through `.git/info/exclude` (no tracked file changes, nothing is committed) and never replaced without a yes. The README and `.env.example` document it. The tests cover every case.
This changes a security guarantee of the earlier milestones ("a token is never persisted"): a token may now be written to one file, and only after a yes. [BC-001] objective 6, success criterion 1 and the risk table are amended in the same change.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | `GITEA_TOKEN` missing from `.env`, or `.env` absent: it is asked, not echoed, validated like a token read from `.env`, and the run continues; the same for `GITHUB_PAT` and `GITHUB_USER` once GitHub is chosen, and never for them when GitHub is not chosen | Tests pass | The run stops with an error, a value is echoed or GitHub credentials are asked without GitHub |
| 2 | An invalid asked value is refused and asked again without showing it; when input ends the run stops before any request to a host and names the key | Tests pass | A request made or a value shown |
| 3 | A credential provided in `.env` is not asked | Tests pass | Any prompt shown |
| 4 | The "create `.env`" question is asked after the local project exists and defaults to no; on no, no file is created and the summary says so | Tests pass | A file written without a yes |
| 5 | On yes the new project's `.env` exists, holds exactly the needed keys (`GITEA_TOKEN`; plus `GITHUB_PAT` and `GITHUB_USER` when GitHub was chosen), has an owner-only mode (600) from the moment it is created, and `git status` in the project does not list it | Tests pass | Another key, another mode or the file listed |
| 6 | An existing `.env` in the project is never replaced without a yes; on no it is kept unchanged and reported | Tests pass | Any replaced without a yes |
| 7 | No token appears in any output, summary, log, remote URL, tracked file or file other than the project's `.env`; searched in every file of the new project and in all output of the tests, including under `bash -x` | Tests pass | Any hit |
| 8 | Only the project's `.env` changed on disk by this feature: no tracked file, no `.gitignore`, no global git configuration is written | Tests pass | Any other change |
| 9 | All acceptance criteria of US-001.05 in [US-001] are met | Verified | Any unmet |
## Dependencies
| Depends on | Reason |
| --- | --- |
| [MIL-004] | Needs the prompt flow and the configuration presets it changed |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| User story US-001.05 | [US-001] |
| Objective 9 (credentials asked, project `.env`) and the amended objective 6 | [BC-001] |
| Success criteria 1 and 9 | [BC-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S02 |
## Target Date
2026-11-27 — proposed; the Business Case sets no deadline.
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Ask for a credential that `.env` does not provide | `.env` becomes optional. `GITEA_TOKEN` is asked after the configuration is read; `GITHUB_PAT` and `GITHUB_USER` once GitHub is chosen. Read without echo (`read -s`), validated by the existing token and account validators, registered for redaction before any later message, asked again when invalid, a stop naming the key when input ends. A new `lib/credentials.sh` (class `CredentialCollector` of [DCD-001]). Extension 2b of [UC-001]. | Yes | [UC-001] |
| 2 | Create the new project's `.env` | After the local project exists and only after a yes (default no): write the needed keys to `.env` created with `umask 077` and mode 600, never replacing an existing file without a yes, and add `.env` to `.git/info/exclude`. Report it in the summary without showing a value. A new `lib/envfile.sh` (class `EnvFileWriter`). Step 9 and extensions 9c and 9d of [UC-001]. | Yes | [UC-001] |
| 3 | Keep every token out of everything else | The `bash -x` guard, the redaction and the temporary files keep working with credentials that are asked; the asked value never reaches a command line, output, summary or a file other than the project's `.env`. Add the new function groups to the library layout. | No | |
| 4 | Document the feature and its risk | README: credentials may be asked, the project `.env`, what it holds, why it is owner-only and ignored, the risk of a plain-text token on disk, how to say no. `.env.example` and the security decisions section updated. | No | |
| 5 | Test every case | Each credential present, missing and invalid; `.env` absent; no GitHub credentials without GitHub; end of input; the question defaults to no; file contents, mode and git exclusion; an existing `.env`; every token searched for in the output and the project; the existing tests unchanged. | No | |
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[DCD-001]: ../uc-001/dcd.md
[MIL-004]: ./mil-004-configurable-details.md
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
@@ -0,0 +1,88 @@
# MIL-006 Project License
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-006 |
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Deprecated | Jens Tirsvad Nielsen | S02 | AGPL-3.0 default only when GitHub is chosen and the project is public (purpose, resolution order, criterion 3, tasks 1 and 4) | [1cd27f7] |
| 2026-10-07 | Accepted | Jens Tirsvad Nielsen | S02 | Task 4 renamed so its issue title is unique (the sync matches issues by title) | [be759e3] |
---
## Purpose
Decide whether the project's license can be set in `config.env` without weakening the existing behaviour: a license that is set applies with or without GitHub, `none` means no license, an absent key gives AGPL-3.0 only when GitHub is chosen and the project is public, and a license the Gitea server does not offer stops the run before anything is created.
## Deliverable
`create-project.sh` that reads one more optional key from `config.env`, `PROJECT_LICENSE`, checks it, resolves the license that applies, checks that Gitea offers it, creates the Gitea repository with it and shows it in the plan and summary. `config.env.example` and the README document the key. The tests cover every case.
The key (present counts as set, as for the other project details of [MIL-004]; an empty value is refused):
| Key | Detail | Accepted value |
| --- | --- | --- |
| `PROJECT_LICENSE` | the license of the project | a Gitea license key (letters, digits, `.`, `+`, `-`, at most 64 characters), such as `AGPL-3.0` or `MIT`, or `none` |
The license that applies is resolved in this order: `PROJECT_LICENSE` when set (`none` means no license), otherwise AGPL-3.0 when GitHub is chosen and the project is public, otherwise none. A private project with GitHub therefore gets no license by default. It is never asked: the key is an optional project detail in `config.env`, not a prompt.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | With `PROJECT_LICENSE` set to a license the server offers, the Gitea repository is created with that license, with GitHub and without it, and the plan and summary show it as coming from `config.env` | Tests pass | Another license, none, or no marker |
| 2 | `PROJECT_LICENSE=none`: the Gitea repository has no license, also when GitHub is chosen | Tests pass | Any license applied |
| 3 | `PROJECT_LICENSE` absent: AGPL-3.0 when GitHub is chosen and the project is public; none for a private project with GitHub, and none without GitHub; the plan and summary say which rule applied | Tests pass | AGPL-3.0 on a private project, or no AGPL-3.0 on a public project with GitHub |
| 4 | An empty or invalid value stops the run before any request to a host, names the key and never falls back to asking | Tests pass | A request made or a prompt shown |
| 5 | A license the Gitea server does not offer stops the run before anything is created and names the license | Tests pass | Anything created |
| 6 | The license is never asked, with the key set, absent or invalid | Tests pass | Any prompt for it |
| 7 | The mirror, the local history and the other steps are unchanged: with GitHub chosen the license file reaches the local project through the Gitea history, as before | Tests pass | Any other step changed |
| 8 | All acceptance criteria of US-001.06 in [US-001] are met | Verified | Any unmet |
## Dependencies
| Depends on | Reason |
| --- | --- |
| [MIL-004] | The key is one more project detail read, validated and marked by the code of the configurable details |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| User story US-001.06 | [US-001] |
| Objective 10 (project license in `config.env`) and the amended objective 2 | [BC-001] |
| Success criterion 10 | [BC-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S02 |
## Target Date
2026-12-04 — proposed; the Business Case sets no deadline.
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Read and validate `PROJECT_LICENSE` | Add the key to the `config.env` parser and its validator (a license key or `none`; empty refused; errors name the key). Resolve the license that applies into the project request (key set, else AGPL-3.0 with GitHub and a public project, else none) and mark it `(from config.env)` in the summary. Never asked. Extensions of step 3 of [UC-001]. | Yes | [UC-001] |
| 2 | Apply the license on Gitea, independent of GitHub | Generalize the preflight check from the fixed AGPL-3.0 to the license that applies (checked only when one applies); create the repository with it; the plan, the summary and the reuse warning name the license that applies instead of AGPL-3.0; GitHub receives the file through the mirror as before. Extension 4c and step 6 of [UC-001]. | Yes | [UC-001] |
| 3 | Document the key | Commented example in `config.env.example`, a row in the README table of project details, and the rule for the license that applies, including `none` and the default. | No | |
| 4 | Test every license case | Key set (with and without GitHub), `none`, absent (public and private, with and without GitHub), empty, invalid and not offered by the server; never asked; the summary marker; the existing tests changed only where a private project with GitHub no longer gets AGPL-3.0. | No | |
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[DCD-001]: ../uc-001/dcd.md
[MIL-004]: ./mil-004-configurable-details.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[be759e3]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/be759e38326eac2b11e65a2b582b2431186e338b
@@ -0,0 +1,85 @@
# MIL-007 Framework Checklists and Usage
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-007 |
| CrossReference | [BC-001], [US-001], [UC-001], [UC-002] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Deprecated | Jens Tirsvad Nielsen | S02 | Task 4 renamed so its issue title is unique (the sync matches issues by title) | [be759e3] |
| 2026-10-07 | Accepted | Jens Tirsvad Nielsen | S02 | The configuration files default to the working folder's, then the checkout's, confirmed and named (deliverable 3, criteria 9 and 10, tasks 2 to 4) | [0ab5006] |
---
## Purpose
Decide whether a new project holds the whole framework, including the `qc` checklists that the framework keeps in its own submodule, and whether a Maintainer can start the script from the folder where the project is to be created, through a global command, with the README saying exactly how.
## Deliverable
1. `create-project.sh` that, after adding the `framework` submodule (and when `framework` already exists as that submodule), runs `git submodule update --init --recursive` in the new project, so `framework/qc/` holds the checklists.
2. `create-project.sh` that finds its own files when started through a symlink, so a link in a folder on `PATH` works from any directory.
3. A README usage section that says: run the script from the folder in which the project is to be created (the default directory is `./<name>` relative to where it is started), keep `config.env` and `.env` in the folder where the project is created or in the checkout (the folder's file wins, each file on its own), name other files with `--config` and `--env`, and make the script global with a worked example (a symlink in a `PATH` folder, with the check that it works), for Linux, macOS and Git Bash on Windows.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | After a run, `framework/qc/` in the new project holds the checklist files and `git submodule status --recursive` shows no entry starting with `-` | Tests pass | An empty `qc` |
| 2 | A second run on a project whose `qc` is empty fills it and changes nothing else; on a complete project it changes nothing | Tests pass | Anything else changed |
| 3 | A failed nested fetch stops the step, reports what exists, names `git submodule update --init --recursive` as the command to run by hand and shows no credential | Tests pass | A credential shown, or the failure not reported |
| 4 | A framework without a submodule of its own does not make the step fail | Tests pass | Step fails |
| 5 | Started through a symlink in another folder, `create-project.sh --help` works and a run creates the project under the current folder | Tests pass | Files not found, or the project created elsewhere |
| 6 | The README example for the global command was run once as written and its check passed | Verified | An example that was not run |
| 7 | The README states the folder to start from and where the new project lands, with an example from a folder that is not the checkout | Reviewed by S02 | Missing or unclear |
| 8 | All acceptance criteria of US-001.07 and US-002 in [US-001] are met | Verified | Any unmet |
| 9 | With `--config` and `--env` absent, `./config.env` and `./.env` in the working folder are used, each file on its own, and the checkout's stand in for a missing one; with neither present the run stops before any request and names both places | Tests pass | A file used from another place, or a request made |
| 10 | A file from the working folder is named with the Gitea address it holds and needs a yes, default no, before the first request; every file used is named in the output | Tests pass | A request before the yes, or a file used without being named |
## Dependencies
| Depends on | Reason |
| --- | --- |
| [MIL-003] | The framework step and the README already exist |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| User stories US-001.07 and US-002 | [US-001] |
| Objective 5 (the framework submodule) | [BC-001] |
| Objective 11 (global command) and success criterion 11 | [BC-001] |
| Objective 7 (documentation) | [BC-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S02 |
## Target Date
2026-12-11 — proposed; the Business Case sets no deadline.
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Fetch the framework's own submodules | After `git submodule add` of the framework, and on the "already a submodule" path, run `git submodule update --init --recursive` in the new project. A failure stops the step, reports what exists and names the command to run by hand, without a credential. Step 9 and extension 9e of [UC-001]. | Yes | [UC-001] |
| 2 | Start through a command link | Resolve `BASH_SOURCE` through links (without requiring `readlink -f`, which macOS lacks) so `SCRIPT_DIR` and `PROJECT_ROOT` point into the checkout, keep the current folder as the base of the default directory, and choose `config.env` and `.env` (named, else `./`, else the checkout's), naming them and asking a yes for a file from the working folder. Errors name the places looked in. Steps 3 to 6 and extensions 4a to 4c of [UC-002]. | Yes | [UC-002] |
| 3 | Document the usage | Step 1 and extensions 1a and 3a of [UC-002]. README usage section: start from the folder where the project is to be created, where `config.env` and `.env` are read from (the folder first, then the checkout), the confirmation of a file from the folder, `--config` and `--env`, and the global command with a worked example and its check, for Linux, macOS and Git Bash on Windows. | Yes | [UC-002] |
| 4 | Test the qc fetch and the command link | `qc` filled after a run and after a rerun, nested fetch failure, framework without a submodule, start through a symlink from another folder; configuration files named, in the folder, in the checkout, mixed and in neither; the confirmation answered yes and no. | No | |
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[UC-002]: ../uc-002/uc.md
[MIL-003]: ./mil-003-scaffold-and-release.md
[be759e3]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/be759e38326eac2b11e65a2b582b2431186e338b
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+24 -8
View File
@@ -4,23 +4,23 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | PP-001 | | ID | PP-001 |
| CrossReference | [BC-001], [SA-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [US-001] | | CrossReference | [BC-001], [SA-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [US-001] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Stories per phase: US-001.01 to US-001.03<br>Dates accepted | [02875ae] | | 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Added phase MIL-006 (proposed dates 2026-11-30 to 2026-12-04) | [d773fa9] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Added phase MIL-004 (proposed dates 2026-11-16 to 2026-11-20) | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added phase MIL-007 (proposed dates 2026-12-07 to 2026-12-11); MIL-006 deliverable names the public-only AGPL-3.0 default | [1cd27f7] |
--- ---
## Purpose ## Purpose
Schedule the four phases that deliver RepoFoundry (`create-project.sh` and its documentation) in dependency order. The Business Case sets no deadline, so the dates below are proposals for S01 to confirm. Schedule the seven phases that deliver RepoFoundry (`create-project.sh` and its documentation) in dependency order. The Business Case sets no deadline, so the dates below are proposals for S01 to confirm.
## Planning Assumptions ## Planning Assumptions
- Week 1 starts 2026-10-05; the plan ends by 2026-11-20 (the last phase is proposed). - Week 1 starts 2026-10-05; the plan ends by 2026-12-11 (the last two phases are proposed).
- Phase length: two weeks. - Phase length: two weeks.
- S01 and S02 review each phase through a pull request, as described in [SA-001]. For now one person holds both roles. - S01 and S02 review each phase through a pull request, as described in [SA-001]. For now one person holds both roles.
- The PO language is English, so no translated copies are kept. - The PO language is English, so no translated copies are kept.
@@ -33,6 +33,9 @@ Schedule the four phases that deliver RepoFoundry (`create-project.sh` and its d
| Repositories and Mirror | [MIL-002] | 2026-10-19 to 2026-10-30 | 2026-10-30 | S02 | US-001.02 | GitHub and Gitea repositories and the push mirror | [Milestone 44] | | Repositories and Mirror | [MIL-002] | 2026-10-19 to 2026-10-30 | 2026-10-30 | S02 | US-001.02 | GitHub and Gitea repositories and the push mirror | [Milestone 44] |
| Scaffold and Release | [MIL-003] | 2026-11-02 to 2026-11-13 | 2026-11-13 | S01 | US-001.03 | Local project, framework, README, final review | [Milestone 45] | | Scaffold and Release | [MIL-003] | 2026-11-02 to 2026-11-13 | 2026-11-13 | S01 | US-001.03 | Local project, framework, README, final review | [Milestone 45] |
| Configurable Details | [MIL-004] | 2026-11-16 to 2026-11-20 | 2026-11-20 | S01 | US-001.04 | Project details preset in config.env | | | Configurable Details | [MIL-004] | 2026-11-16 to 2026-11-20 | 2026-11-20 | S01 | US-001.04 | Project details preset in config.env | |
| Credentials | [MIL-005] | 2026-11-23 to 2026-11-27 | 2026-11-27 | S01 | US-001.05 | Missing credentials asked; project .env | |
| Project License | [MIL-006] | 2026-11-30 to 2026-12-04 | 2026-12-04 | S01 | US-001.06 | PROJECT_LICENSE in config.env; AGPL-3.0 default only for a public GitHub project | |
| Framework Checklists and Usage | [MIL-007] | 2026-12-07 to 2026-12-11 | 2026-12-11 | S01 | US-001.07 | qc fetched with the framework; global command; README usage | |
```plantuml ```plantuml
@startgantt @startgantt
@@ -45,6 +48,12 @@ Project starts 2026-10-05
[Scaffold and Release Go/No-Go] happens 2026-11-13 [Scaffold and Release Go/No-Go] happens 2026-11-13
[Configurable Details] starts 2026-11-16 and ends 2026-11-20 [Configurable Details] starts 2026-11-16 and ends 2026-11-20
[Configurable Details Go/No-Go] happens 2026-11-20 [Configurable Details Go/No-Go] happens 2026-11-20
[Credentials] starts 2026-11-23 and ends 2026-11-27
[Credentials Go/No-Go] happens 2026-11-27
[Project License] starts 2026-11-30 and ends 2026-12-04
[Project License Go/No-Go] happens 2026-12-04
[Framework Checklists and Usage] starts 2026-12-07 and ends 2026-12-11
[Framework Checklists and Usage Go/No-Go] happens 2026-12-11
@endgantt @endgantt
``` ```
@@ -59,11 +68,15 @@ Project starts 2026-10-05
| Local directory, remotes, framework submodule, skills, hooks, templates, plan gate | [MIL-003] | | Local directory, remotes, framework submodule, skills, hooks, templates, plan gate | [MIL-003] |
| README and SSH prerequisite documentation | [MIL-003] | | README and SSH prerequisite documentation | [MIL-003] |
| Project details set in `config.env` instead of asked | [MIL-004] | | Project details set in `config.env` instead of asked | [MIL-004] |
| Missing credentials asked; the new project's `.env` | [MIL-005] |
| Project license set in `config.env` | [MIL-006] |
| Framework's own submodules fetched | [MIL-007] |
| README usage from the target folder and as a global command | [MIL-007] |
## Dependencies ## Dependencies
``` ```
MIL-001 → MIL-002 → MIL-003 → MIL-004 MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007
``` ```
A No-Go moves every later date by the time needed to rework the failed criteria. A No-Go moves every later date by the time needed to rework the failed criteria.
@@ -91,11 +104,14 @@ A No-Go moves every later date by the time needed to rework the failed criteria.
[MIL-002]: ./milestones/mil-002-repositories-and-mirror.md [MIL-002]: ./milestones/mil-002-repositories-and-mirror.md
[MIL-003]: ./milestones/mil-003-scaffold-and-release.md [MIL-003]: ./milestones/mil-003-scaffold-and-release.md
[MIL-004]: ./milestones/mil-004-configurable-details.md [MIL-004]: ./milestones/mil-004-configurable-details.md
[MIL-005]: ./milestones/mil-005-credentials.md
[MIL-006]: ./milestones/mil-006-project-license.md
[MIL-007]: ./milestones/mil-007-framework-checklists.md
[US-001]: ./user-stories.md [US-001]: ./user-stories.md
[UC-001]: ./uc-001/uc.md [UC-001]: ./uc-001/uc.md
[SSD-001]: ./uc-001/ssd.md [SSD-001]: ./uc-001/ssd.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[Milestone 43]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/43 [Milestone 43]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/43
[Milestone 44]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/44 [Milestone 44]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/44
[Milestone 45]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/45 [Milestone 45]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/45
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+74
View File
@@ -0,0 +1,74 @@
# SQA Review Record: MIL-005 and the planning change it causes
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-020 |
| CrossReference | [MIL-005], [QC-MIL-001], [US-001], [UC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [ded26a6] |
---
## Artifact Under Review
- Instance reviewed: [MIL-005], and the changes it causes in [BC-001], [US-001], [UC-001], [SSD-001], [OC-001], [SD-001], [DM-001], [DM-002], [DICT-001] and [PP-001]. The two Design Class Diagrams that change with it are reviewed in [RC-021].
- Checklist used: [QC-MIL-001] for [MIL-005]. The other artifacts were changed, not created; their change is checked below for consistency with the checklists of their types and with the PP reference.
- Review date: 2026-10-06
## Checklist Results ([MIL-005], QC-MIL-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | A script that asks for a missing credential and creates the new project's `.env`, the documentation of the feature and its risk, and tests for every case. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Nine criteria, each with an objective Go and No-Go, including all acceptance criteria of US-001.05. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Depends on [MIL-004], with the reason. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Maps to objective 9, the amended objective 6 and success criteria 1 and 9 of [BC-001], and to US-001.05. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Owner S01, approving reviewer S02. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | 2026-11-27 matches [PP-001]; the Business Case sets no duration, so nothing conflicts. The date is a proposal and is accepted here. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [BC-001] | Objective 9, scope items, success criterion 9, a risk and a constraint; objective 6 and success criterion 1 amended | Pass | The security guarantee is weakened on purpose and said so in the same place: a token may be written only to the new project's `.env`, after a yes. The risk row lists the mitigations. |
| [US-001] | US-001.05 with five acceptance criteria | Pass | Given/when/then; traces to [UC-001] and [MIL-005]; the last criterion keeps the "no credential in output" rule. |
| [UC-001] | Precondition, step 2 and 9 notes, extensions 2b, 9c, 9d, a postcondition and two business rules | Pass | Extension 2b ends before any change when input ends; 9c and 9d keep "never replaced without a yes". |
| [SSD-001] | `writeEnvFile` and the credentials not provided in `.env` become parameters; the lifecycle note names the one thing that persists | Pass | One new parameter and a note; the operations are unchanged. |
| [OC-001] | Postcondition P14, a precondition, two exceptions, and P2 reworded | Pass | P14 states the contents, the mode, the git exclusion and "no credential shown". |
| [SD-001] | `CredentialCollector` and `EnvFileWriter` and their messages | Pass | Every new postcondition has a message; the coverage table is updated. Three lines damaged by an earlier edit (`actor Maintainer`, the first `provideProjectDetails` message, the final `summary` return) are restored in this change. |
| [DM-001], [DM-002] | Concept `Credentials File` and two associations | Pass | Both models changed in the same way. |
| [DICT-001] | `Credentials File` ↔ `EnvFile` | Pass | One PO term and one IT term, as the dictionary rules require. |
| [PP-001] | Phase [MIL-005], dependency chain, timeline | Pass | Dates agree with [MIL-005]. |
One point for the implementation: the Go/No-Go criterion 5 says the file has mode 600 "from the moment it is created". That means the script must create it under `umask 077` (or with `install -m 600`) and not write it first and restrict it afterwards. Task 2 already says so.
## Overall Verdict
Go — [MIL-005] passes every mandatory criterion and the changes to the other artifacts are consistent with each other. The weakening of the credential guarantee is deliberate, bounded and recorded in the Business Case. Author and reviewer are the same person for now (S01 and S02 are both held by the Maintainer), so the framework independence rule is not met; re-review when a second person takes S02.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | - | - |
---
[MIL-005]: ../../milestones/mil-005-credentials.md
[BC-001]: ../../business-case.md
[US-001]: ../../user-stories.md
[UC-001]: ../../uc-001/uc.md
[SSD-001]: ../../uc-001/ssd.md
[OC-001]: ../../uc-001/oc.md
[SD-001]: ../../uc-001/sd.md
[DM-001]: ../../uc-001/dm.md
[DM-002]: ../../domain-model.md
[DICT-001]: ../../dictionary.md
[PP-001]: ../../project-plan.md
[RC-021]: ./rc-021-dcd.md
[QC-MIL-001]: ../../../framework/qc/qc-milestones-gateways.md
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+64
View File
@@ -0,0 +1,64 @@
# SQA Review Record: Design Class Diagrams DCD-001 and DCD-002
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-021 |
| CrossReference | [DCD-001], [DCD-002], [QC-DCD-001], [SD-001], [OC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [ded26a6] |
---
## Artifact Under Review
- Instances reviewed: [DCD-001] (use case UC-001) and [DCD-002] (the consolidated project model). [DCD-002] was created from [DCD-001] and the two have the same classes, relationships and tables.
- Checklist used: [QC-DCD-001]
- Review date: 2026-10-06
- PlantUML: the diagrams were not rendered. `render-diagrams.sh` needs a PlantUML server and sends the diagram text to it; none is configured. The syntax was read by hand (see finding F3).
## Checklist Results
| # | Criterion | Level | Status | Evidence/Notes |
| --- | --- | --- | --- | --- |
| 1 | SOLID principles applied; no god classes | Mandatory | Pass | Each class has one reason to change: one host API each (`GiteaClient`, `GitHubClient`), local work (`LocalProjectBuilder`), the framework (`FrameworkInstaller`), prompts for credentials (`CredentialCollector`), the project `.env` (`EnvFileWriter`), the report (`SummaryReport`). `ProjectCreator` has two operations and no data. The clients share `GitHost` instead of repeating its three operations. |
| 2 | Visibility markers correct and consistent | Mandatory | Pass | Every attribute and operation has `+` or `-`; the only private operation is `GiteaClient.requestSync`, which no other class calls. Enumeration literals carry no marker, as is usual. |
| 3 | Association, aggregation, composition and dependency correctly distinguished | Mandatory | Pass | Composition where the part cannot outlive the whole (`Run`, `Configuration`, `LocalProject` and their parts); plain association for the links between independent objects; dependency for "creates" and "asks"; generalization for `Repository` and `GitHost`. No aggregation is used. |
| 4 | Multiplicities and navigability specified on all associations | Mandatory | Pass after fix | Found during this review: most associations gave only the target multiplicity. Fixed: both ends now carry a multiplicity and every association has one arrow. Dependencies carry none, as UML does not give them one. |
| 5 | Applied design patterns annotated | Optional | Pass | The Pattern Annotations table names Controller, Facade, Pure Fabrication, Creator, Protection from variations and a data transfer object. |
| 6 | Method signatures traceable to Operation Contracts and Sequence Diagrams | Mandatory | Pass | The Method Traceability table has a row for each of the 20 operations, each naming the [SD-001] message and the contract postcondition. [SD-001] was aligned in the same change (`createRepository(request, license)`, `compose(request)` and others). |
| 7 | Class names consistent with the Domain Model concepts they refine | Mandatory | Pass | The IT terms of [DICT-001] are used (`ProjectRequest` for Project, `Credential` for Access Token, `EnvFile` for Credentials File, and so on). `GitHost` is a class of its own, as the dictionary has one IT term for the PO term. The system concepts without a PO term (`Run`, `ToolCheck`, `PreflightResult`, `PromptSet`, `InstallResult`) are marked as such in the class table. |
| 8 | No circular dependencies | Optional | Pass | Stated and argued in the Dependency Check; `Summary` points at `ProjectRequest` and nothing points back; the helper classes depend on the data classes and the controller, never the other way round. |
## Findings
| # | Finding | Severity | Status |
| --- | --- | --- | --- |
| F1 | Associations gave only the target multiplicity (criterion 4). | Defect | Fixed in both files. |
| F2 | The planned classes `CredentialCollector`, `EnvFileWriter` and `EnvFile` (milestone [MIL-005]) are already in the diagram while the code does not exist yet. | Info | Accepted: the Implementation Mapping marks them "planned for MIL-005", so the diagram is a design for code still to come. |
| F3 | The PlantUML text was not rendered by a tool. | Low | Open: render with `render-diagrams.sh` once a server is chosen. Constructs used are standard (`abstract class`, `enum`, stereotypes, multiplicities, `skinparam`, `hide empty members`). |
| F4 | `Credential` is also the kind of `GITHUB_USER`, which is an account name, not a secret. The dictionary maps Access Token to `Credential`. | Info | Accepted: `kind` tells them apart; `Configuration` holds one to three of them. |
## Overall Verdict
Go — all mandatory criteria pass after the fix for criterion 4, and the optional ones pass. F3 is open and not blocking. Author and reviewer are the same person for now (S01 and S02 are both held by the Maintainer), so the framework independence rule is not met; re-review when a second person takes S02.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Render the diagrams of [DCD-001], [DCD-002], [SD-001] and the other PlantUML blocks with `render-diagrams.sh` once the Maintainer chooses a server | S01 | 2026-10-16 |
---
[DCD-001]: ../../uc-001/dcd.md
[DCD-002]: ../../dcd.md
[SD-001]: ../../uc-001/sd.md
[OC-001]: ../../uc-001/oc.md
[DICT-001]: ../../dictionary.md
[MIL-005]: ../../milestones/mil-005-credentials.md
[QC-DCD-001]: ../../../framework/qc/qc-dcd.md
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+72
View File
@@ -0,0 +1,72 @@
# SQA Review Record: MIL-007 and the planning changes it causes
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-022 |
| CrossReference | [MIL-007], [QC-MIL-001], [MIL-006], [BC-001], [US-001], [UC-002], [PP-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
---
## Artifact Under Review
- Instance reviewed: [MIL-007], and the changes it causes in [MIL-006], [BC-001], [US-001], [PP-001] and [TM-001]. The use case and its artifacts are reviewed in [RC-023], [RC-024], [RC-025], [RC-026], [RC-027] and [RC-028].
- Checklist used: [QC-MIL-001] for [MIL-007]. The other artifacts were changed, not created; their change is checked below.
- Review date: 2026-10-07
## Checklist Results (QC-MIL-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | The script that fetches the framework's own submodules and starts through a command link, plus the README usage section. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Eight criteria, each with an objective Go and No-Go, including every acceptance criterion of US-001.07 and US-002. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Depends on [MIL-003] (the framework step and README exist), with the reason. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Objectives 5, 7 and 11 and success criterion 11 of [BC-001]; US-001.07 and US-002. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Owner S01, approving reviewer S02. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | 2026-12-11 matches [PP-001]; the Business Case sets no deadline. A proposal for S01 to confirm. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [MIL-006] | AGPL-3.0 default only when GitHub is chosen and the project is public (purpose, order, criterion 3, tasks 1 and 4) | Pass | Criterion 3 is objective in both directions: AGPL-3.0 on a private project is a No-Go, and so is none on a public project with GitHub. Tasks and tests follow. |
| [BC-001] | Objectives 2, 5, 10, 11; scope items; success criteria 10 and 11 | Pass | Objective 2 and criterion 10 name the public-only default; objective 11 and criterion 11 are new and measurable. |
| [US-001] | US-001.06 reworded; US-001.07 and US-002 added; story count updated | Pass | Given/when/then; each traces to its use case and milestone; INVEST exceptions stated. |
| [PP-001] | Phase MIL-007, dependency chain, timeline, scope coverage, end date 2026-12-11 | Pass | Dates agree with [MIL-007]. One point carried over from earlier plans: the assumption "phase length: two weeks" is not true of [MIL-005], [MIL-006] and [MIL-007], which are one week. |
| [TM-001] | Rows for MIL-007 and the UC-002 artifacts | Pass | Upstream and downstream columns agree with the CrossReference of each document. |
## Overall Verdict
Go — [MIL-007] meets every mandatory criterion and the changes to the other artifacts are consistent. Drafted by Claude Code for S02 against the checklist; the author and reviewer are the same person for now (as in the earlier records). The verdict takes effect, and the Version History rows of the reviewed documents change to `Accepted`, only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Say in the plan assumptions that the later phases are one week, not two (or correct the phase length) | S01 | Before [MIL-006] starts |
| Confirm the proposed target date 2026-12-11 | S01 | Before [MIL-007] starts |
---
[MIL-007]: ../../milestones/mil-007-framework-checklists.md
[MIL-006]: ../../milestones/mil-006-project-license.md
[BC-001]: ../../business-case.md
[US-001]: ../../user-stories.md
[UC-002]: ../../uc-002/uc.md
[PP-001]: ../../project-plan.md
[TM-001]: ../traceability-matrix.md
[RC-023]: ./rc-023-uc-002.md
[RC-024]: ./rc-024-ssd-002.md
[RC-025]: ./rc-025-dm-003.md
[RC-026]: ./rc-026-oc-002.md
[RC-027]: ./rc-027-sd-002.md
[RC-028]: ./rc-028-dcd-003.md
[QC-MIL-001]: ../../../framework/qc/qc-milestones-gateways.md
[MIL-003]: ../../milestones/mil-003-scaffold-and-release.md
[MIL-005]: ../../milestones/mil-005-credentials.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+63
View File
@@ -0,0 +1,63 @@
# SQA Review Record: UC-002 Start the script as a global command
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-023 |
| CrossReference | [UC-002], [QC-UC-001], [UC-001], [UCD-001], [US-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
---
## Artifact Under Review
- Instance reviewed: [UC-002], and the changes it causes in [UC-001] and [UCD-001].
- Checklist used: [QC-UC-001] (Fully Dressed).
- Review date: 2026-10-07
## Checklist Results (QC-UC-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Consists of a single, concise paragraph summarizing only the primary success scenario | N-A | Format is Fully Dressed. |
| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | Format is Fully Dressed. |
| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | Pass | Actor, stakeholders, preconditions, postconditions, main scenario, extensions, rules and open issues are present. |
| 4 | Preconditions and postconditions are explicitly defined | Pass | Preconditions name the checkout, the config files and a writable `PATH` folder; postconditions name the link, the working folder and that nothing else was written. |
| 5 | Primary actor is explicitly stated | Pass | Primary actor: Maintainer, as in [UCD-001]. |
| 6 | Stakeholders and their interests are stated | Pass | S01, S02 and S03 with their interests. |
| 7 | Main success scenario is written as clear, numbered steps | Pass | Six numbered steps; steps 1 and 2 are the Maintainer's, 3 to 6 the system's. |
| 8 | Alternative/exception flows correctly reference `<<include>>`/`<<extend>>` use cases where relevant, per UML 2.5.1 | Pass | Step 5 `<<include>>` [UC-001], and [UCD-001] shows the same relationship. |
| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text | Pass | Five rules, each tied to a step. |
| 10 | Naming of actors and use case title is consistent with the corresponding Use Case Diagram and User Stories | Pass | Title "Start the script as a global command" and actor equal those of [UCD-001] and US-002. |
| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated | Pass | Level user-goal and scope are stated. |
| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Written as the Maintainer's goal. It names `PATH`, `--config` and `--env`, which are the Maintainer's own vocabulary for this system, as in [UC-001]; the rule that named a missing tool was reworded to "on every supported platform" during this review. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [UC-001] | Step 6 and rule 6 (public-only AGPL-3.0 default); step 9 and extension 9e (the framework's own submodules) | Pass | Extension 9e stops the step and names the command to run by hand, without a credential; consistent with US-001.07 and MIL-007. |
| [UCD-001] | UC-002 added with `<<include>>` [UC-001] | Pass | Diagram, actor table, use case table and relationship table agree. |
## Overall Verdict
Go — [UC-002] meets every mandatory criterion. Drafted by Claude Code for S02 against the checklist; the author and reviewer are the same person for now (as in the earlier records). The verdict takes effect, and the Version History rows of the reviewed documents change to `Accepted`, only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | - | - |
---
[UC-002]: ../../uc-002/uc.md
[UC-001]: ../../uc-001/uc.md
[UCD-001]: ../../use-case-diagram.md
[US-001]: ../../user-stories.md
[QC-UC-001]: ../../../framework/qc/qc-use-case.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+50
View File
@@ -0,0 +1,50 @@
# SQA Review Record: SSD-002 Start the script as a global command
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-024 |
| CrossReference | [SSD-002], [QC-SSD-001], [UC-002], [DM-003] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
---
## Artifact Under Review
- Instance reviewed: [SSD-002].
- Checklist used: [QC-SSD-001].
- Review date: 2026-10-07
## Checklist Results (QC-SSD-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Input/output messages match the corresponding Use Case's main success scenario step-for-step | Pass | `startFromWorkingFolder` covers steps 3 to 5 and `provideProjectDetails` steps 5 and 6; steps 1 and 2 are the Maintainer's own and the diagram says so. |
| 2 | Actor and System are treated strictly as black boxes (system shown as `:System`) | Pass | Only `Maintainer` and `:System` appear. |
| 3 | Object creation/destruction of the System instance handled explicitly where relevant | N-A | The system is one script run. |
| 4 | Return values are shown for operations that produce one, using dashed return arrows | Pass | Dashed returns for the prompts and the summary. |
| 5 | Alternate/exceptional flows are represented separately (or explicitly out of scope noted) | Pass | Failure flows are named as out of scope (extensions 1a, 3a, 4a, 4b). Added during this review: the first draft did not say so. |
| 6 | Message names are verb phrases consistent with the use case's system responsibilities | Pass | Both messages are verb phrases. |
| 7 | Diagram references the specific Use Case (name and ID) it depicts | Pass | Names [UC-002] in Source Use Case. |
## Overall Verdict
Go — [SSD-002] meets every mandatory criterion after the fix recorded under criterion 5. Drafted by Claude Code for S02 against the checklist; the author and reviewer are the same person for now (as in the earlier records). The verdict takes effect, and the Version History rows of the reviewed documents change to `Accepted`, only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | - | - |
---
[SSD-002]: ../../uc-002/ssd.md
[UC-002]: ../../uc-002/uc.md
[DM-003]: ../../uc-002/dm.md
[QC-SSD-001]: ../../../framework/qc/qc-ssd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+74
View File
@@ -0,0 +1,74 @@
# SQA Review Record: DM-003 Start the script as a global command, and the dictionary
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-025 |
| CrossReference | [DM-003], [QC-DM-001], [QC-DICT-001], [DICT-001], [DM-002] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
---
## Artifact Under Review
- Instance reviewed: [DM-003], and the changes it causes in [DM-002] and [DICT-001].
- Checklists used: [QC-DM-001] for [DM-003]; [QC-DICT-001] for [DICT-001].
- Review date: 2026-10-07
## Checklist Results (QC-DM-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Uses ubiquitous/business language throughout; no technical or implementation jargon (e.g. no "table", "class", "pointer") | Pass | Command Link, Checkout and Working Folder are Maintainer terms. "Checkout" is also a git word; it is kept because it names the folder the Maintainer cloned. |
| 2 | Multiplicities on associations are correct and complete (e.g. `1..*`, `0..1`) | Pass | Every association has a multiplicity on both ends. |
| 3 | No operation/method signatures shown — attributes and associations only | Pass | Attributes and associations only. |
| 4 | Associations are named with an unambiguous reading direction | Pass | Every association has a reading direction in the table. |
| 5 | Generalization/specialization used correctly, reflecting true "is-a" relationships, not misused for code reuse | N-A | No generalization. |
| 6 | Every concept traces to a noun phrase found in the use cases or glossary | Pass | Each new concept cites its use case step. The concepts Maintainer, Configuration and Local Project come from [DM-001] and are stated as not redefined. |
| 7 | Attributes are simple domain data (no foreign-key-like references or object pointers modeled as attributes) | Pass | `folder` and `path` are simple data; the links to other concepts are associations. |
## Checklist Results (QC-DICT-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Every row has a PO term, its language, an IT term and a definition | Pass | Three new rows, each with PO term, language, IT term and definition. |
| 2 | Each PO term maps to exactly one IT term and the reverse (no synonyms) | Pass | One IT term per PO term and the reverse: Command Link / CommandLink, Checkout / Checkout, Working Folder / WorkingFolder. |
| 3 | Every Domain Model concept has a row, and the Domain Model uses its PO term | Pass | Every concept of [DM-003] and of [DM-002] has a row. |
| 4 | The Operation Contracts, Sequence Diagrams, Design Class Diagrams and ERD use the IT term, not the PO term | Pass | [OC-002], [SD-002] and [DCD-003] use the IT terms. `CommandLink` appears in [OC-002] only: it is a link the Maintainer makes, so it has no design class, which the DCD and the dictionary say. |
| 5 | Definitions are written in the PO language and are one sentence | Pass | One-sentence definitions in English, the PO language. |
| 6 | "Used as PO term in" and "Used as IT term in" name artifact types that exist in the project | Pass | Names DM, UC, OC, SD and DCD, which exist. |
| 7 | Translated artifacts (`<artifact>.<language>.md`) use the PO terms of the dictionary | N-A | The PO language is English. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [DM-002] | Three concepts and five associations added | Pass | Diagram, concept table and association table changed in the same way as [DM-003]; Purpose names [UC-002]. |
## Overall Verdict
Go — [DM-003] and the dictionary meet every mandatory criterion. Drafted by Claude Code for S02 against the checklist; the author and reviewer are the same person for now (as in the earlier records). The verdict takes effect, and the Version History rows of the reviewed documents change to `Accepted`, only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | - | - |
---
[DM-003]: ../../uc-002/dm.md
[DM-002]: ../../domain-model.md
[DM-001]: ../../uc-001/dm.md
[DICT-001]: ../../dictionary.md
[OC-002]: ../../uc-002/oc.md
[SD-002]: ../../uc-002/sd.md
[DCD-003]: ../../uc-002/dcd.md
[QC-DM-001]: ../../../framework/qc/qc-domain-model.md
[QC-DICT-001]: ../../../framework/qc/qc-dictionary.md
[UC-002]: ../../uc-002/uc.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+58
View File
@@ -0,0 +1,58 @@
# SQA Review Record: OC-002 Start the script as a global command, and OC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-026 |
| CrossReference | [OC-002], [QC-OC-001], [OC-001], [SSD-002], [DM-003] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
---
## Artifact Under Review
- Instance reviewed: [OC-002], and the changes it causes in [OC-001].
- Checklist used: [QC-OC-001].
- Review date: 2026-10-07
## Checklist Results (QC-OC-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Method signature is complete: operation name, parameter types, and return type | Pass | `startFromWorkingFolder(configPath: Path [0..1], envPath: Path [0..1]): PromptSet`, as in [SSD-002]. |
| 2 | Preconditions explicitly list required state before execution | Pass | Two preconditions in Domain Model terms (a Command Link or the path, and a Working Folder). |
| 3 | Postconditions explicitly describe resulting state using Larman's "instance created/associated/attribute modified" style | Pass | Six postconditions as created, associated or set. P2 was reworded during this review from "by following the link through every level of links" (a procedure) to "reached through the link, however many links lie between them". |
| 4 | Exceptions and error conditions are documented, including the triggering precondition failure | Pass | Three exceptions, each with its failing precondition and an outcome that changes nothing. |
| 5 | Operation is explicitly traceable to a single SSD message | Pass | One contract for `startFromWorkingFolder`; `provideProjectDetails` is the contract of [OC-001] and the SSD says so. |
| 6 | Contract avoids specifying implementation/algorithmic details (declarative, not procedural) | Pass | Declarative after the P2 rewording. |
| 7 | Cross-references the Domain Model classes/associations affected by pre/postconditions | Pass | The Concepts row and the preamble name the concepts of [DM-003]. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [OC-001] | P4: the AGPL-3.0 default needs `githubOwner` and a public `visibility`; P10: the framework's own submodules were initialised; a new exception for a failed fetch | Pass | The exception outcome matches extension 9e of [UC-001] and names the command to run by hand. |
## Overall Verdict
Go — [OC-002] meets every mandatory criterion. Drafted by Claude Code for S02 against the checklist; the author and reviewer are the same person for now (as in the earlier records). The verdict takes effect, and the Version History rows of the reviewed documents change to `Accepted`, only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | - | - |
---
[OC-002]: ../../uc-002/oc.md
[OC-001]: ../../uc-001/oc.md
[SSD-002]: ../../uc-002/ssd.md
[DM-003]: ../../uc-002/dm.md
[QC-OC-001]: ../../../framework/qc/qc-operation-contract.md
[UC-001]: ../../uc-001/uc.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+51
View File
@@ -0,0 +1,51 @@
# SQA Review Record: SD-002 Start the script as a global command
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-027 |
| CrossReference | [SD-002], [QC-SD-001], [OC-002], [DCD-003] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
---
## Artifact Under Review
- Instance reviewed: [SD-002].
- Checklist used: [QC-SD-001].
- Review date: 2026-10-07
## Checklist Results (QC-SD-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Message passing strictly follows UML sync/async/return arrow syntax | Pass | Solid arrows for calls, dashed for returns. |
| 2 | GRASP/GoF patterns applied and explicitly annotated where used (e.g. Controller, Observer, Mediator, Factory) | Pass | Information Expert, Controller and Low Coupling are annotated. |
| 3 | Lifelines show activation bars matching actual processing time/call nesting | Pass | `Launcher` and `ProjectCreator` are activated for the length of their calls. |
| 4 | Object creation and destruction shown with correct UML notation (`create`/`destroy` messages, X on lifeline) | Pass | `create` messages for `Checkout` and `WorkingFolder` were missing in the first draft and were added during this review. |
| 5 | Diagram realizes the postconditions of a specific Operation Contract | Pass | The coverage table maps P1 to P6 to messages. |
| 6 | Responsibility assignment favors low coupling/high cohesion (no god-object receiving all messages) | Pass | `Launcher` only finds the checkout and the working folder; the other work stays with `ProjectCreator`. |
| 7 | Loop, alt, and opt combined fragments used correctly for conditional/repeated behavior | N-A | No loop or option: two `alt` fragments for the exceptions. |
| 8 | Each exception of the realized Operation Contract is shown as an `alt` or `opt` fragment, or its absence is justified | Pass | Both exception groups of [OC-002] are `alt` fragments and the coverage table lists them. |
## Overall Verdict
Go — [SD-002] meets every mandatory criterion after the fix recorded under criterion 4. Drafted by Claude Code for S02 against the checklist; the author and reviewer are the same person for now (as in the earlier records). The verdict takes effect, and the Version History rows of the reviewed documents change to `Accepted`, only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | - | - |
---
[SD-002]: ../../uc-002/sd.md
[OC-002]: ../../uc-002/oc.md
[DCD-003]: ../../uc-002/dcd.md
[QC-SD-001]: ../../../framework/qc/qc-sequence-diagram.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+63
View File
@@ -0,0 +1,63 @@
# SQA Review Record: DCD-003 Start the script as a global command, and DCD-002
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-028 |
| CrossReference | [DCD-003], [QC-DCD-001], [DCD-002], [DCD-001], [SD-002] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
---
## Artifact Under Review
- Instance reviewed: [DCD-003], and the changes it causes in [DCD-002].
- Checklist used: [QC-DCD-001].
- Review date: 2026-10-07
## Checklist Results (QC-DCD-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | SOLID principles applied; no god classes with excessive responsibilities | Pass | Three small classes with one responsibility each; `Launcher` has no data and no logic beyond finding folders. |
| 2 | Visibility markers correct and consistent (`+` public, `-` private, `#` protected) | Pass | `+` for operations, `-` for attributes. |
| 3 | Relationships correctly distinguished: Association vs Aggregation vs Composition vs Dependency | Pass | Composition `Run` to `Checkout` and `WorkingFolder`; dependencies for creation; one association `Launcher` to `ProjectCreator`. |
| 4 | Multiplicities and navigability specified on all associations | Pass | The `Launcher` to `ProjectCreator` association had no multiplicity in the first draft; `"1" --> "1"` was added in [DCD-003] and [DCD-002] during this review. |
| 5 | Applied design patterns are annotated explicitly (e.g. Singleton, Factory, Strategy) | Pass | Controller and Information Expert are annotated. |
| 6 | Method signatures are traceable to Operation Contracts and/or design Sequence Diagrams | Pass | Method Traceability maps every method to [OC-002] or [SD-002]. |
| 7 | Class names and structure remain consistent with the Domain Model concepts they refine | Pass | Launcher refines Command Link, Checkout and Working Folder keep their names; the table says why Command Link has no class of its own. |
| 8 | No circular dependencies between classes/packages unless explicitly justified | Pass | `Launcher` depends on the others and none depends on it. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [DCD-002] | `Launcher`, `Checkout`, `WorkingFolder`; `ProjectCreator.startProjectCreation` takes the checkout, the working folder and the two paths | Pass with a note | The signature differs from [DCD-001], [OC-001] and [SD-001], which show `startProjectCreation()` with no parameters. The scoped views of [UC-001] were not changed; [DCD-002] is the consolidated truth and records the new signature and its source in Method Traceability. |
## Overall Verdict
Go-with-conditions — [DCD-003] meets every mandatory criterion, but the consolidated [DCD-002] and the scoped views of [UC-001] disagree on the signature of `startProjectCreation`. The status stays `Proposed` until the action item is closed. Drafted by Claude Code for S02 against the checklist; the author and reviewer are the same person for now (as in the earlier records). The verdict takes effect, and the Version History rows of the reviewed documents change to `Accepted`, only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Decide whether [OC-001], [SD-001] and [DCD-001] get the new `startProjectCreation` parameters too, or are kept as the scoped view of [UC-001] with a note that [DCD-002] supersedes the signature | S01 | Before [MIL-007] starts |
---
[DCD-003]: ../../uc-002/dcd.md
[DCD-002]: ../../dcd.md
[DCD-001]: ../../uc-001/dcd.md
[OC-001]: ../../uc-001/oc.md
[SD-001]: ../../uc-001/sd.md
[SD-002]: ../../uc-002/sd.md
[UC-001]: ../../uc-001/uc.md
[MIL-007]: ../../milestones/mil-007-framework-checklists.md
[QC-DCD-001]: ../../../framework/qc/qc-dcd.md
[OC-002]: ../../uc-002/oc.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
@@ -0,0 +1,83 @@
# SQA Review Record: Default configuration files from the working folder
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-029 |
| CrossReference | [MIL-007], [QC-MIL-001], [UC-002], [BC-001], [US-001], [DM-003], [DM-002], [OC-002], [SD-002], [DCD-003], [DCD-002], [DICT-001], [SSD-002] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [0ab5006] |
---
## Artifact Under Review
- Instance reviewed: the change that makes `./config.env` and `./.env` in the working folder the default configuration files (after `--config` and `--env`, before the checkout's). It touches [MIL-007] and, through it, [BC-001], [US-001], [UC-002], [SSD-002], [DM-003], [DM-002], [OC-002], [SD-002], [DCD-003], [DCD-002] and [DICT-001]. It follows [RC-022] to [RC-028], which reviewed the first version of these documents; where this record disagrees with them, this record applies.
- Checklist used: [QC-MIL-001] for [MIL-007]; the checklists of the other types are applied to the changed parts below.
- Review date: 2026-10-07
## Checklist Results ([MIL-007], QC-MIL-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | Deliverable 3 now names the folder-first lookup; the code and tests are named in tasks 2 and 4. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Criteria 9 and 10 are new and objective: which file is used, that nothing is requested before the yes, and that every file used is named. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Unchanged: [MIL-003]. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Objective 11 and success criterion 11 of [BC-001] now carry the lookup rule. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Unchanged: S01 and S02. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Unchanged: 2026-12-11. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [UC-002] | Precondition, step 4, extensions 4b and 4c, and two rules | Pass | Each file is chosen on its own: named, else `./`, else the checkout's. Extension 4c asks for a yes (default no) before the first request. The use case stays free of implementation detail. |
| [SSD-002] | One sentence: the confirmation is out of scope | Pass | Same convention as the consent questions of [SSD-001]. |
| [DM-003], [DM-002] | Association Working Folder "may hold" Configuration, 1 to 0..1; Working Folder definition | Pass | Both models changed in the same way; multiplicity on both ends. |
| [OC-002] | `ConfigFiles` and a new P5; former P5 and P6 become P6 and P7; two exceptions | Pass | Declarative: the chosen paths are stated as values, not as a search procedure. Each exception names its failing precondition. |
| [SD-002] | `locateConfigFiles`, creation of `ConfigFiles`, a confirmation `alt`, a new signature of `startProjectCreation` | Pass | Every postcondition has a message in the coverage table; `create` is shown for `ConfigFiles`. |
| [DCD-003], [DCD-002] | `ConfigFiles`; `Launcher.locateConfigFiles`; `WorkingFolder.configFile` and `envFile`; the signature of `startProjectCreation` | Pass with a note | Method Traceability covers each new method. The signature of `startProjectCreation` now differs more from [DCD-001], [OC-001] and [SD-001]; this widens the action item of [RC-028]. |
| [DICT-001] | `ConfigFiles` named as a system concept without a PO term | Pass | Treated like `Run` and `PromptSet`, as the dictionary rules allow. |
| [BC-001], [US-001] | Objective 11, scope item and criterion 11; the acceptance criteria of US-002 | Pass | Given/when/then; the confirmation and the naming of files are testable. |
## Risk found in the change
A `config.env` in the working folder can set `GITEA_URL` to another host, and the token from `.env` would then be sent there on the first request. That is why [UC-002] extension 4c and [MIL-007] criterion 10 require the files and the Gitea address to be named and a yes before any request. The yes does not protect a Maintainer who confirms without reading, and it adds one prompt to every run that uses a folder file; if S01 finds the prompt too heavy, the alternative is to confirm only when the address in the folder's `config.env` differs from the checkout's.
## Overall Verdict
Go-with-conditions — the change is consistent across the documents, and the security risk above is answered by a criterion that can be tested. The status stays `Proposed` until the action items are closed. Drafted by Claude Code for S02; the author and reviewer are the same person for now. The verdict takes effect only when S02 confirms it.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Decide whether the confirmation is asked on every run that uses a folder file, or only when the Gitea address differs from the checkout's | S01 | Before [MIL-007] starts |
| Settle the `startProjectCreation` signature in [OC-001], [SD-001] and [DCD-001], as in the action item of [RC-028] | S01 | Before [MIL-007] starts |
---
[MIL-007]: ../../milestones/mil-007-framework-checklists.md
[QC-MIL-001]: ../../../framework/qc/qc-milestones-gateways.md
[UC-002]: ../../uc-002/uc.md
[BC-001]: ../../business-case.md
[US-001]: ../../user-stories.md
[DM-003]: ../../uc-002/dm.md
[DM-002]: ../../domain-model.md
[OC-002]: ../../uc-002/oc.md
[SD-002]: ../../uc-002/sd.md
[DCD-003]: ../../uc-002/dcd.md
[DCD-002]: ../../dcd.md
[DICT-001]: ../../dictionary.md
[SSD-002]: ../../uc-002/ssd.md
[SSD-001]: ../../uc-001/ssd.md
[OC-001]: ../../uc-001/oc.md
[SD-001]: ../../uc-001/sd.md
[DCD-001]: ../../uc-001/dcd.md
[RC-022]: ./rc-022-mil-007.md
[RC-028]: ./rc-028-dcd-003.md
[MIL-003]: ../../milestones/mil-003-scaffold-and-release.md
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+48 -16
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version, UC-001 artifacts and baseline | [02875ae] | | 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Added MIL-006 | [d773fa9] |
| 2026-10-05 | Proposed | Jens Tirsvad Nielsen | S02 | Added MIL-004 and RC-018 | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added MIL-007, UC-002, SSD-002, DM-003, OC-002, SD-002 and DCD-003 with their reviews RC-022 to RC-028 | [1cd27f7] |
--- ---
@@ -24,27 +24,38 @@ updated whenever an artifact instance is created or reviewed.
| Artifact Instance | Type | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) | | Artifact Instance | Type | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| [BC-001] | BC | - | [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [US-001], [UCD-001] | [RC-010], [RC-018] | | [BC-001] | BC | - | [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [US-001], [UCD-001] | [RC-010], [RC-018], [RC-020], [RC-022], [RC-029] |
| [SA-001] | SA | [BC-001] | [UCD-001], [UC-001], [DICT-001] | [RC-013] | | [SA-001] | SA | [BC-001] | [UCD-001], [UC-001], [DICT-001] | [RC-013] |
| [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004] | [RC-012], [RC-018] | | [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007] | [RC-012], [RC-018], [RC-020], [RC-022] |
| [MIL-001] | MIL | [BC-001], [PP-001] | [US-001] | [RC-011], [RC-016] | | [MIL-001] | MIL | [BC-001], [PP-001] | [US-001] | [RC-011], [RC-016] |
| [MIL-002] | MIL | [BC-001], [PP-001] | [US-001] | [RC-014], [RC-017] | | [MIL-002] | MIL | [BC-001], [PP-001] | [US-001] | [RC-014], [RC-017] |
| [MIL-003] | MIL | [BC-001], [PP-001] | [US-001] | [RC-015], [RC-017] | | [MIL-003] | MIL | [BC-001], [PP-001] | [US-001] | [RC-015], [RC-017] |
| [MIL-004] | MIL | [BC-001], [PP-001] | [US-001] | [RC-018], [RC-019] | | [MIL-004] | MIL | [BC-001], [PP-001] | [US-001] | [RC-018], [RC-019] |
| [UCD-001] | UCD | [BC-001], [SA-001] | [US-001], [UC-001] | [RC-009] | | [MIL-005] | MIL | [BC-001], [PP-001] | [US-001] | [RC-020] |
| [US-001] | US | [BC-001], [UCD-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004] | [UC-001] | [RC-001] | | [MIL-006] | MIL | [BC-001], [PP-001] | [US-001] | [RC-022] |
| [UC-001] | UC | [UCD-001], [US-001], [SA-001] | [SSD-001], [DM-001] | [RC-002] | | [MIL-007] | MIL | [BC-001], [PP-001] | [US-001], [UC-002] | [RC-022], [RC-029] |
| [SSD-001] | SSD | [UC-001] | [OC-001] | [RC-003] | | [UCD-001] | UCD | [BC-001], [SA-001] | [US-001], [UC-001], [UC-002] | [RC-009], [RC-023] |
| [DM-001] | DM | [UC-001], [SSD-001] | [DM-002], [DICT-001], [OC-001] | [RC-004] | | [US-001] | US | [BC-001], [UCD-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007] | [UC-001] | [RC-001], [RC-020], [RC-022], [RC-029] |
| [DM-002] | DM | [DM-001] | [DICT-001] | [RC-005] | | [UC-001] | UC | [UCD-001], [US-001], [SA-001] | [SSD-001], [DM-001] | [RC-002], [RC-020], [RC-023] |
| [DICT-001] | DICT | [BC-001], [SA-001], [DM-001], [DM-002] | [OC-001], [SD-001] | [RC-008] | | [UC-002] | UC | [UCD-001], [US-001], [SA-001], [BC-001] | [SSD-002], [DM-003] | [RC-023], [RC-029] |
| [OC-001] | OC | [SSD-001], [DM-001] | [SD-001] | [RC-006] | | [SSD-002] | SSD | [UC-002], [DM-003] | [OC-002] | [RC-024], [RC-029] |
| [SD-001] | SD | [OC-001] | - | [RC-007] | | [DM-003] | DM | [UC-002], [UCD-001], [SSD-002], [DICT-001], [DM-001] | [OC-002], [DCD-003] | [RC-025], [RC-029] |
| [OC-002] | OC | [SSD-002], [DM-003] | [SD-002] | [RC-026], [RC-029] |
| [SD-002] | SD | [OC-002], [DCD-003] | [DCD-003] | [RC-027], [RC-029] |
| [DCD-003] | DCD | [DM-003], [SD-002], [DICT-001], [UC-002], [DCD-001] | [DCD-002] | [RC-028], [RC-029] |
| [SSD-001] | SSD | [UC-001] | [OC-001] | [RC-003], [RC-020] |
| [DM-001] | DM | [UC-001], [SSD-001] | [DM-002], [DICT-001], [OC-001], [DCD-001] | [RC-004], [RC-020] |
| [DM-002] | DM | [DM-001] | [DICT-001], [DCD-001], [DCD-002] | [RC-005], [RC-020], [RC-025], [RC-029] |
| [DICT-001] | DICT | [BC-001], [SA-001], [DM-001], [DM-002] | [OC-001], [SD-001] | [RC-008], [RC-020], [RC-025], [RC-029] |
| [OC-001] | OC | [SSD-001], [DM-001] | [SD-001] | [RC-006], [RC-020], [RC-026] |
| [SD-001] | SD | [OC-001] | [DCD-001] | [RC-007], [RC-020], [RC-021] |
| [DCD-001] | DCD | [UC-001], [DM-001], [DM-002], [OC-001], [SD-001], [DICT-001] | [DCD-002] | [RC-021] |
| [DCD-002] | DCD | [DCD-001], [DCD-003], [DM-002], [DICT-001] | - | [RC-021], [RC-028], [RC-029] |
## Coverage Notes ## Coverage Notes
- Reviewed so far: every artifact in the project (see the Last Reviewed column). - Reviewed so far: every artifact in the project (see the Last Reviewed column).
- No Design Class Diagram, ERD, KPI, BMC or BPMN exists yet. `-` in Downstream means nothing is built on the artifact yet. - No ERD, KPI, BMC or BPMN exists yet. `-` in Downstream means nothing is built on the artifact yet.
--- ---
@@ -55,11 +66,32 @@ updated whenever an artifact instance is created or reviewed.
[MIL-002]: ../milestones/mil-002-repositories-and-mirror.md [MIL-002]: ../milestones/mil-002-repositories-and-mirror.md
[MIL-003]: ../milestones/mil-003-scaffold-and-release.md [MIL-003]: ../milestones/mil-003-scaffold-and-release.md
[MIL-004]: ../milestones/mil-004-configurable-details.md [MIL-004]: ../milestones/mil-004-configurable-details.md
[MIL-005]: ../milestones/mil-005-credentials.md
[MIL-006]: ../milestones/mil-006-project-license.md
[MIL-007]: ../milestones/mil-007-framework-checklists.md
[RC-018]: ./reviews/rc-018-mil-004.md [RC-018]: ./reviews/rc-018-mil-004.md
[RC-019]: ./reviews/rc-019-mil-004-code.md [RC-019]: ./reviews/rc-019-mil-004-code.md
[RC-020]: ./reviews/rc-020-mil-005.md
[RC-021]: ./reviews/rc-021-dcd.md
[RC-022]: ./reviews/rc-022-mil-007.md
[RC-023]: ./reviews/rc-023-uc-002.md
[RC-024]: ./reviews/rc-024-ssd-002.md
[RC-025]: ./reviews/rc-025-dm-003.md
[RC-026]: ./reviews/rc-026-oc-002.md
[RC-027]: ./reviews/rc-027-sd-002.md
[RC-028]: ./reviews/rc-028-dcd-003.md
[RC-029]: ./reviews/rc-029-default-config-files.md
[DCD-001]: ../uc-001/dcd.md
[DCD-002]: ../dcd.md
[UCD-001]: ../use-case-diagram.md [UCD-001]: ../use-case-diagram.md
[US-001]: ../user-stories.md [US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md [UC-001]: ../uc-001/uc.md
[UC-002]: ../uc-002/uc.md
[SSD-002]: ../uc-002/ssd.md
[DM-003]: ../uc-002/dm.md
[OC-002]: ../uc-002/oc.md
[SD-002]: ../uc-002/sd.md
[DCD-003]: ../uc-002/dcd.md
[SSD-001]: ../uc-001/ssd.md [SSD-001]: ../uc-001/ssd.md
[DM-001]: ../uc-001/dm.md [DM-001]: ../uc-001/dm.md
[DM-002]: ../domain-model.md [DM-002]: ../domain-model.md
@@ -83,5 +115,5 @@ updated whenever an artifact instance is created or reviewed.
[RC-015]: ./reviews/rc-015-mil-003.md [RC-015]: ./reviews/rc-015-mil-003.md
[RC-016]: ./reviews/rc-016-create-project-sh.md [RC-016]: ./reviews/rc-016-create-project-sh.md
[RC-017]: ./reviews/rc-017-e2e-security-review.md [RC-017]: ./reviews/rc-017-e2e-security-review.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+341
View File
@@ -0,0 +1,341 @@
# Design Class Diagram (UC-001)
## Metadata
| Key | Value |
| --- | --- |
| ID | DCD-001 |
| CrossReference | [UC-001], [DM-001], [DM-002], [OC-001], [SD-001], [DICT-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added CredentialCollector, EnvFileWriter and EnvFile; writeEnvFile parameter | [ded26a6] |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | ProjectRequest carries the license that applies | [d773fa9] |
---
## Purpose and Scope
Covers [UC-001] "Create a new project". It refines the concepts of [DM-001] into design classes and turns the messages of [SD-001] into method signatures, so that every method traces to a contract in [OC-001] or to a message in [SD-001]. Class and attribute names are the IT terms of [DICT-001]. The project-level model that consolidates all use cases is [DCD-002].
The classes are design classes of a Bash program: a class is a group of functions in `src/lib/` with its data held in the shared state arrays (see "Implementation Mapping"). There is no object-oriented runtime, but the responsibilities, associations and dependencies below are the ones the code keeps.
Failure handling (the exceptions of [OC-001]) is one rule of `ProjectCreator`, stop and report, and is not drawn.
## Diagram
```plantuml
@startuml
skinparam classAttributeIconSize 0
hide empty members
enum Visibility {
private
public
}
class ProjectCreator <<controller>> {
+startProjectCreation() : PromptSet
+provideProjectDetails(name : String, description : String, visibility : Visibility, giteaOwner : Owner, githubOwner : Owner [0..1], directory : Path, enablePlanGate : Boolean, writeEnvFile : Boolean) : Summary
}
class ConfigLoader {
+load(configFile : Path, envFile : Path) : Configuration
}
class CredentialCollector {
+collect(configuration : Configuration, kinds : String [1..3]) : Configuration
}
class EnvFileWriter {
+write(project : LocalProject, configuration : Configuration, hasGithub : Boolean) : EnvFile [0..1]
}
class ToolChecker {
+check(tools : String [1..*]) : ToolCheck
}
class Preflight {
+check(request : ProjectRequest) : PreflightResult
}
abstract class GitHost <<facade>> {
-name : String
-webAddress : String
-apiAddress : String
+verifyToken() : Boolean
+ownerAccepts(owner : Owner) : Boolean
+nameFree(name : String) : Boolean
}
class GiteaClient <<facade>> {
+GiteaClient(configuration : Configuration)
+hasLicense(key : String) : Boolean
+createRepository(request : ProjectRequest, license : String [0..1]) : GiteaRepository
+addPushMirror(source : GiteaRepository, target : GitHubRepository) : PushMirror
-requestSync(mirror : PushMirror) : void
}
class GitHubClient <<facade>> {
+GitHubClient(configuration : Configuration)
+createEmptyRepository(request : ProjectRequest) : GitHubRepository
}
class LocalProjectBuilder {
+build(directory : Path, source : GiteaRepository, target : GitHubRepository [0..1], sshPassed : Boolean) : LocalProject
}
class FrameworkInstaller {
+install(project : LocalProject, enablePlanGate : Boolean) : InstallResult
}
class SummaryReport {
+compose(request : ProjectRequest) : Summary
}
class Run {
-isApply : Boolean
}
class Configuration {
-giteaUrl : String
-giteaApiUrl : String
-githubWebUrl : String
-githubApiUrl : String
-giteaSshPort : Integer
-mirrorInterval : String
-frameworkRepo : String
-presetDetails : Map [0..1]
}
class Credential {
-kind : String
-value : String
}
class ToolCheck {
-hasGit : Boolean
-hasCurl : Boolean
-hasJq : Boolean
}
class PromptSet {
-prompts : String [1..*]
}
class ProjectRequest {
-name : String
-description : String
-visibility : Visibility
-directory : Path
-enablePlanGate : Boolean
-license : String [0..1]
}
class Owner {
-name : String
-kind : String
}
class PreflightResult {
-tokensWork : Boolean
-ownersAccept : Boolean
-nameIsFree : Boolean
-licenseIsOffered : Boolean
-sshPassed : Boolean
}
abstract class Repository {
-name : String
-description : String
-visibility : Visibility
-address : String
}
class GiteaRepository
class GitHubRepository
class LicenseFile {
-key : String
}
class PushMirror {
-interval : String
-syncOnCommit : Boolean
}
class LocalProject {
-directory : Path
}
class Remote {
-name : String
-address : String
}
class Submodule {
-name : String
-address : String
}
class HookSetup {
-areSkillsInstalled : Boolean
-areHooksInstalled : Boolean
-isPlanGateEnabled : Boolean
}
class EnvFile {
-address : Path
-keys : String [1..3]
}
class Template {
-name : String
-isCopied : Boolean
}
class InstallResult <<dto>>
class Summary {
-createdItems : String [0..*]
-skippedItems : String [0..*]
-nextSteps : String [0..*]
}
ProjectCreator ..> ConfigLoader : creates
ProjectCreator ..> ToolChecker : creates
ProjectCreator ..> CredentialCollector : creates
ProjectCreator ..> EnvFileWriter : creates [0..1]
ProjectCreator ..> Preflight : creates
ProjectCreator ..> GiteaClient : creates
ProjectCreator ..> GitHubClient : creates [0..1]
ProjectCreator ..> LocalProjectBuilder : creates
ProjectCreator ..> FrameworkInstaller : creates
ProjectCreator ..> SummaryReport : creates
Preflight ..> GiteaClient : asks
Preflight ..> GitHubClient : asks [0..1]
GitHost <|-- GiteaClient
GitHost <|-- GitHubClient
GitHost "1" --> "0..*" Owner : has
GiteaClient ..> Configuration
GitHubClient ..> Configuration
ProjectCreator "0..*" --> "1" Run
Run "1" *-- "1" Configuration
Run "1" *-- "1" ToolCheck
Run "1" *-- "0..1" ProjectRequest
Run "1" --> "1" PromptSet : returns
Configuration "1" *-- "1..3" Credential
ProjectRequest "0..*" --> "1" Owner : giteaOwner
ProjectRequest "0..*" --> "0..1" Owner : githubOwner
ProjectRequest "1" *-- "0..1" PreflightResult
ProjectRequest "1" --> "0..1" GiteaRepository : stored in
ProjectRequest "1" --> "0..1" GitHubRepository : also stored in
ProjectRequest "1" --> "0..1" LocalProject : working copy
Summary "0..*" --> "1" ProjectRequest : reports on
Repository <|-- GiteaRepository
Repository <|-- GitHubRepository
Repository "0..*" --> "1" Owner : owned by
GiteaRepository "1" *-- "0..1" LicenseFile
PushMirror "0..*" --> "1" GiteaRepository : source
PushMirror "0..*" --> "1" GitHubRepository : target
PushMirror "0..*" --> "1" Credential : authorised by
LocalProject "1" *-- "1..2" Remote
Remote "0..*" --> "1" Repository : points to
LocalProject "1" *-- "1" Submodule
LocalProject "1" *-- "1" HookSetup
LocalProject "1" *-- "0..*" Template
LocalProject "1" *-- "0..1" EnvFile
EnvFile "0..*" --> "1..3" Credential : copy of
InstallResult "0..*" --> "1" Submodule
InstallResult "0..*" --> "1" HookSetup
InstallResult "0..*" --> "0..*" Template
ProjectRequest "0..*" --> "1" Visibility
Repository "0..*" --> "1" Visibility
@enduml
```
## Class Table
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
| --- | --- | --- | --- | --- |
| `ProjectCreator` | none (controller for the system operations of [OC-001]) | Receives the two system operations, sequences the steps and stops on the first failure. | none | `startProjectCreation`, `provideProjectDetails` |
| `ConfigLoader` | Configuration | Reads `config.env` and `.env` as plain text and validates every value, preset project details included. | none | `load` |
| `CredentialCollector` | none (system concept) | Asks, without echo, for a credential that `.env` does not provide and validates it like one read from `.env`. | none | `collect` |
| `EnvFileWriter` | Credentials File | Creates the project's own `.env` with the credentials the project needs: owner-only, excluded from git, never replaced without a yes. | none | `write` |
| `ToolChecker` | none (system concept `ToolCheck`) | Detects the required and optional tools. | none | `check` |
| `Preflight` | none (system concept `PreflightResult`) | Runs the read-only checks of both hosts before anything is created. | none | `check` |
| `GitHost` | Git Host | The operations every host offers: check the token, check that an owner accepts new repositories, check that a name is free. | `name`, `webAddress`, `apiAddress` | `verifyToken`, `ownerAccepts`, `nameFree` |
| `GiteaClient` | Git Host (Gitea) | Hides the Gitea API and its token; creates the repository and the push mirror. | none beyond `GitHost` (uses `Configuration`) | `GiteaClient`, `hasLicense`, `createRepository`, `addPushMirror`, `requestSync` |
| `GitHubClient` | Git Host (GitHub) | Hides the GitHub API and its token; creates the empty repository. | none beyond `GitHost` (uses `Configuration`) | `GitHubClient`, `createEmptyRepository` |
| `LocalProjectBuilder` | Local Project, Remote | Creates the project directory, its git repository and its credential-free remotes. | none | `build` |
| `FrameworkInstaller` | Framework, Framework Setup, Template | Adds the framework submodule, installs skills and hooks once, and copies the templates without overwriting. | none | `install` |
| `SummaryReport` | Summary | Composes the report of what was created, skipped or failed. | none | `compose` |
| `Run` | none (system concept) | Holds the state of one execution. | `isApply` | none |
| `Configuration` | Configuration | Holds the service addresses, the credentials and any preset project details. | `giteaUrl`, `giteaApiUrl`, `githubWebUrl`, `githubApiUrl`, `giteaSshPort`, `mirrorInterval`, `frameworkRepo`, `presetDetails` | none |
| `Credential` | Access Token | Holds a secret in memory only; it never becomes part of an address or a message. | `kind`, `value` | none |
| `ToolCheck` | none (system concept) | Records which tools are present. | `hasGit`, `hasCurl`, `hasJq` | none |
| `PromptSet` | none (system concept) | The questions still to ask; a detail preset in `config.env` is not in it. | `prompts` | none |
| `ProjectRequest` | Project | Holds the details of the project being created. | `name`, `description`, `visibility`, `directory`, `enablePlanGate`, `license` | none |
| `Owner` | Owner | A user or organization on a host. | `name`, `kind` | none |
| `PreflightResult` | none (system concept) | Records the outcome of the preflight checks. | `tokensWork`, `ownersAccept`, `nameIsFree`, `licenseIsOffered`, `sshPassed` | none |
| `Repository` | Repository | Common data of a repository on a host. | `name`, `description`, `visibility`, `address` | none |
| `GiteaRepository` | Gitea Repository | The source of truth. | none beyond `Repository` | none |
| `GitHubRepository` | GitHub Repository | Receives its content from the mirror. | none beyond `Repository` | none |
| `LicenseFile` | License | The license file in the Gitea repository when a license applies. | `key` | none |
| `PushMirror` | Mirror | The Gitea to GitHub push mirror. | `interval`, `syncOnCommit` | none |
| `LocalProject` | Local Project | The project directory on the Maintainer's machine. | `directory` | none |
| `Remote` | Remote | A named link to a repository (`origin`, `github`), without a credential. | `name`, `address` | none |
| `Submodule` | Framework | The framework added to the local project. | `name`, `address` | none |
| `HookSetup` | Framework Setup | Records the skills and hooks installed and the plan gate state. | `areSkillsInstalled`, `areHooksInstalled`, `isPlanGateEnabled` | none |
| `EnvFile` | Credentials File | The `.env` of the project: a copy of the credentials it needs. | `address`, `keys` | none |
| `Template` | Template | A framework file copied into the project. | `name`, `isCopied` | none |
| `InstallResult` | none (carries the result of one operation) | Returns the submodule, the hook setup and the templates of `install`. | none | none |
| `Summary` | Summary | The report returned to the Maintainer; it contains no credential. | `createdItems`, `skippedItems`, `nextSteps` | none |
| `Visibility` | none (enumeration of a Project and Repository attribute) | The two allowed visibilities. | `private`, `public` | none |
## Method Traceability
| Method signature | Operation Contract / SD message |
| --- | --- |
| `ProjectCreator.startProjectCreation() : PromptSet` | [OC-001] `startProjectCreation`; [SD-001] `startProjectCreation()` |
| `ProjectCreator.provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile) : Summary` | [OC-001] `provideProjectDetails`; [SD-001] `provideProjectDetails(...)` |
| `ConfigLoader.load(configFile, envFile) : Configuration` | [SD-001] `load(config.env, .env)`; [OC-001] `startProjectCreation` P2 |
| `CredentialCollector.collect(configuration, kinds) : Configuration` | [SD-001] `collect(configuration, GITEA_TOKEN)` and `collect(configuration, GITHUB_PAT, GITHUB_USER)`; [OC-001] `startProjectCreation` P2 and the precondition of `provideProjectDetails` |
| `EnvFileWriter.write(project, configuration, hasGithub) : EnvFile` | [SD-001] `write(localProject, configuration, githubOwner present)`; [OC-001] `provideProjectDetails` P14 |
| `ToolChecker.check(tools) : ToolCheck` | [SD-001] `check(git, curl, jq)`; [OC-001] `startProjectCreation` P3 |
| `Preflight.check(request) : PreflightResult` | [SD-001] `check(request)`; [OC-001] `provideProjectDetails` P2 |
| `GiteaClient(configuration)` | [SD-001] `new(configuration)` to `GiteaClient` |
| `GitHost.verifyToken() : Boolean` | [SD-001] `verifyToken()` from `Preflight` to either client; P2 |
| `GitHost.ownerAccepts(owner) : Boolean` | [SD-001] `ownerAccepts(giteaOwner)` and `ownerAccepts(githubOwner)`; P2 |
| `GitHost.nameFree(name) : Boolean` | [SD-001] `nameFree(name)` to either client; P2 |
| `GiteaClient.hasLicense(key) : Boolean` | [SD-001] `hasLicense(license)`; P2 |
| `GiteaClient.createRepository(request, license) : GiteaRepository` | [SD-001] `createRepository(request, license)`; P3, P4 |
| `GiteaClient.addPushMirror(source, target) : PushMirror` | [SD-001] `addPushMirror(giteaRepository, gitHubRepository)`; P6 |
| `GiteaClient.requestSync(mirror) : void` | [SD-001] `requestSync(pushMirror)`; P6 |
| `GitHubClient(configuration)` | [SD-001] `new(configuration)` to `GitHubClient` |
| `GitHubClient.createEmptyRepository(request) : GitHubRepository` | [SD-001] `createEmptyRepository(request)`; P5 |
| `LocalProjectBuilder.build(directory, source, target, sshPassed) : LocalProject` | [SD-001] `build(directory, giteaRepository, gitHubRepository, sshPassed)`; P7, P8, P9 |
| `FrameworkInstaller.install(project, enablePlanGate) : InstallResult` | [SD-001] `install(localProject, enablePlanGate)`; P10, P11, P12 |
| `SummaryReport.compose(request) : Summary` | [SD-001] `compose(projectRequest)`; P13 |
## Pattern Annotations
| Pattern | Classes | Rationale |
| --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | One entry for the system operations; coordinates and does no HTTP, git or file work itself |
| Facade (GoF) | `GitHost`, `GiteaClient`, `GitHubClient` | Each client hides one host's HTTP API and keeps the token inside; no other class sees a credential. `GitHost` holds the operations both share |
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker`, `CredentialCollector`, `EnvFileWriter`, `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport` | No domain concept owns these responsibilities; small units keep cohesion high |
| Creator (GRASP) | `ConfigLoader` creates `Configuration`; `GiteaClient` creates `GiteaRepository` and `PushMirror` | The creating class holds the data needed to build the object |
| Protection from variations (GRASP) | `GiteaClient`, `GitHubClient`, `ProjectRequest` | The optional GitHub path is decided by the controller; the clients do not know it |
| Data Transfer Object (GoF-style) | `InstallResult` | Carries the three results of `install` in one return value |
## Dependency Check
No circular dependency. `ProjectCreator` depends on every helper class and no helper depends on it. `Preflight` depends on the two clients; the clients extend `GitHost` and depend only on `Configuration`. The data classes form a tree: `Run` holds `Configuration`, `ToolCheck` and `ProjectRequest`; `ProjectRequest` reaches the repositories and the `LocalProject`; `Summary` points at `ProjectRequest` and nothing points back at it. `CredentialCollector` and `EnvFileWriter` depend only on `Configuration`, `Credential` and `LocalProject`; the only class that holds a secret after the run is `EnvFile`, and only as a copy written to the Maintainer's own disk. `Repository` is shared by `Remote` and `PushMirror` without a cycle.
SOLID check: no class has more than one reason to change (one host API, one kind of local work, one report); the clients can be replaced behind the same operations; the controller depends on the operations, not on how a host or git is called. `ProjectCreator` has two operations and no data, so it is not a god class.
## Implementation Mapping
| Design class | Where it lives in `src/` |
| --- | --- |
| `ProjectCreator` | `create-project.sh` (`main`), `lib/apply.sh` |
| `ConfigLoader` | `lib/config.sh` (`load_configuration`), `lib/validate.sh` |
| `ToolChecker` | `lib/tools.sh` |
| `CredentialCollector` | planned for [MIL-005]: `lib/credentials.sh`, with `lib/prompts.sh` |
| `EnvFileWriter` | planned for [MIL-005]: `lib/envfile.sh` |
| `Preflight` | `lib/preflight.sh` |
| `GitHost`, `GiteaClient`, `GitHubClient` | `lib/api.sh`, `lib/http.sh`, `lib/json.sh`, `lib/repositories.sh`, `lib/mirror.sh`, `lib/hosts.sh` |
| `LocalProjectBuilder` | `lib/localproject.sh`, `lib/git.sh` |
| `FrameworkInstaller` | `lib/framework.sh` |
| `SummaryReport` | `lib/steps.sh`, `lib/plan.sh` |
| `PromptSet`, `ProjectRequest` | `lib/project.sh`, `lib/prompts.sh` |
| `Run`, `Configuration`, `Credential`, `PreflightResult` | the state arrays declared in `lib/constants.sh` |
---
[UC-001]: ./uc.md
[DM-001]: ./dm.md
[DM-002]: ../domain-model.md
[OC-001]: ./oc.md
[SD-001]: ./sd.md
[MIL-005]: ../milestones/mil-005-credentials.md
[DICT-001]: ../dictionary.md
[DCD-002]: ../dcd.md
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
[d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
+14 -6
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] | | 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | License applies when configured, not only when GitHub is chosen | [d773fa9] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Configuration may hold preset project details | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | License: the AGPL-3.0 default needs GitHub and a public project | [1cd27f7] |
--- ---
@@ -79,6 +79,9 @@ class "Framework Setup" as FrameworkSetup {
class Template { class Template {
name name
} }
class "Credentials File" as CredentialsFile {
address
}
class Summary { class Summary {
created items created items
skipped items skipped items
@@ -108,6 +111,8 @@ LocalProject "1" --> "1" FrameworkSetup : has
FrameworkSetup "0..*" --> "1" Framework : is installed from FrameworkSetup "0..*" --> "1" Framework : is installed from
Framework "1" --> "1..*" Template : provides Framework "1" --> "1..*" Template : provides
LocalProject "1" --> "0..*" Template : contains a copy of LocalProject "1" --> "0..*" Template : contains a copy of
LocalProject "1" --> "0..1" CredentialsFile : has
CredentialsFile "1" --> "1..2" AccessToken : holds a copy of
Summary "1" --> "1" Project : reports on Summary "1" --> "1" Project : reports on
@enduml @enduml
``` ```
@@ -125,13 +130,14 @@ Summary "1" --> "1" Project : reports on
| Repository | A place on a Git Host that holds a project's history | name, description, visibility, address | [UC-001] steps 5 and 6 "repository" | | Repository | A place on a Git Host that holds a project's history | name, description, visibility, address | [UC-001] steps 5 and 6 "repository" |
| Gitea Repository | The Repository on Gitea; the source of truth | none beyond Repository | [UC-001] step 6 | | Gitea Repository | The Repository on Gitea; the source of truth | none beyond Repository | [UC-001] step 6 |
| GitHub Repository | The Repository on GitHub; receives its content from the Mirror | none beyond Repository | [UC-001] step 5 | | GitHub Repository | The Repository on GitHub; receives its content from the Mirror | none beyond Repository | [UC-001] step 5 |
| License | The legal terms file added to a Gitea Repository (AGPL-3.0) when GitHub is chosen | name | [UC-001] step 6 "AGPL license" | | License | The legal terms file added to a Gitea Repository when a license applies: the one set in the Configuration, or AGPL-3.0 when GitHub is chosen, the project is public and none is set | name | [UC-001] step 6 "license" |
| Mirror | The push mirror that copies a Gitea Repository to a GitHub Repository | interval, sync on commit | [UC-001] step 7 "push mirror" | | Mirror | The push mirror that copies a Gitea Repository to a GitHub Repository | interval, sync on commit | [UC-001] step 7 "push mirror" |
| Local Project | The project directory on the Maintainer's machine | directory | [UC-001] step 8 "local project" | | Local Project | The project directory on the Maintainer's machine | directory | [UC-001] step 8 "local project" |
| Remote | A named link from a Local Project to a Repository (`origin`, `github`) | name, address | [UC-001] step 8 "remote" | | Remote | A named link from a Local Project to a Repository (`origin`, `github`) | name, address | [UC-001] step 8 "remote" |
| Framework | The SQA-QC-Framework added to a Local Project | name, address | [UC-001] step 9 "framework submodule" | | Framework | The SQA-QC-Framework added to a Local Project | name, address | [UC-001] step 9 "framework submodule" |
| Framework Setup | The skills and git hooks installed from the Framework, with the plan gate on or off | plan gate enabled | [UC-001] step 9 "skills and hooks", "plan gate" | | Framework Setup | The skills and git hooks installed from the Framework, with the plan gate on or off | plan gate enabled | [UC-001] step 9 "skills and hooks", "plan gate" |
| Template | A file the Framework provides to copy into a project (`AGENTS.md`, artifact registry) | name | [UC-001] step 9 "templates" | | Template | A file the Framework provides to copy into a project (`AGENTS.md`, artifact registry) | name | [UC-001] step 9 "templates" |
| Credentials File | The file in a Local Project that holds a copy of the Access Tokens (and the GitHub account name) the project needs; readable by its owner only and ignored by git | address | [UC-001] step 9 "credentials file" |
| Summary | The report of what was created, skipped or failed and how to continue | created items, skipped items, next steps | [UC-001] step 10 "summary" | | Summary | The report of what was created, skipped or failed and how to continue | created items, skipped items, next steps | [UC-001] step 10 "summary" |
## Association Table ## Association Table
@@ -146,7 +152,7 @@ Summary "1" --> "1" Project : reports on
| Owner | owns | Repository | 1 to 0..* | | Owner | owns | Repository | 1 to 0..* |
| Project | is stored in | Gitea Repository | 1 to 1 | | Project | is stored in | Gitea Repository | 1 to 1 |
| Project | is also stored in | GitHub Repository | 1 to 0..1 | | Project | is also stored in | GitHub Repository | 1 to 0..1 |
| Gitea Repository | has | License | 1 to 0..1 (1 when GitHub is chosen) | | Gitea Repository | has | License | 1 to 0..1 (1 when a license applies) |
| Mirror | copies from | Gitea Repository | 1 to 1 | | Mirror | copies from | Gitea Repository | 1 to 1 |
| Mirror | copies to | GitHub Repository | 1 to 1 | | Mirror | copies to | GitHub Repository | 1 to 1 |
| Mirror | is authorised by | Access Token | 0..* to 1 | | Mirror | is authorised by | Access Token | 0..* to 1 |
@@ -158,6 +164,8 @@ Summary "1" --> "1" Project : reports on
| Framework Setup | is installed from | Framework | 0..* to 1 | | Framework Setup | is installed from | Framework | 0..* to 1 |
| Framework | provides | Template | 1 to 1..* | | Framework | provides | Template | 1 to 1..* |
| Local Project | contains a copy of | Template | 1 to 0..* | | Local Project | contains a copy of | Template | 1 to 0..* |
| Local Project | has | Credentials File | 1 to 0..1 |
| Credentials File | holds a copy of | Access Token | 1 to 1..2 |
| Summary | reports on | Project | 1 to 1 | | Summary | reports on | Project | 1 to 1 |
## Generalizations ## Generalizations
@@ -172,5 +180,5 @@ Summary "1" --> "1" Project : reports on
[SSD-001]: ./ssd.md [SSD-001]: ./ssd.md
[DICT-001]: ../dictionary.md [DICT-001]: ../dictionary.md
[DM-002]: ../domain-model.md [DM-002]: ../domain-model.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+18 -12
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] | | 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | P2, P4 and an exception: the license that applies, not always AGPL-3.0 | [d773fa9] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Project details may be defined by the Configuration | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | P4: the AGPL-3.0 default needs githubOwner and a public visibility; P10: the framework's own submodules were initialised, with an exception for a failed fetch | [1cd27f7] |
--- ---
@@ -31,7 +31,7 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
**Postconditions** **Postconditions**
- P1. A `Run` instance was created. - P1. A `Run` instance was created.
- P2. A `Configuration` instance was created from `config.env` and `.env`, with every value validated (including the project details preset in `config.env`) and the credentials held only in memory. - P2. A `Configuration` instance was created from `config.env` and `.env`, with every value validated (including the project details preset in `config.env`). A `Credential` that `.env` did not provide was entered by the Maintainer without echo and validated; every `Credential` is held only in memory.
- P3. A `ToolCheck` instance was created and associated with the `Run`, recording that `git` and `curl` are present and whether `jq` is present. - P3. A `ToolCheck` instance was created and associated with the `Run`, recording that `git` and `curl` are present and whether `jq` is present.
- P4. The `Run` was associated with a `PromptSet` that is returned. - P4. The `Run` was associated with a `PromptSet` that is returned.
@@ -39,49 +39,55 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
| Condition (failing precondition) | Outcome | | Condition (failing precondition) | Outcome |
| --- | --- | | --- | --- |
| `config.env` or `.env` is missing, or a value is missing or malformed (a preset project detail included) | The `Run` ends with an error naming the key, never its value; nothing was changed | | `config.env` is missing, or a value in `config.env` or `.env` is malformed (a preset project detail included) | The `Run` ends with an error naming the key, never its value; nothing was changed |
| A `Credential` is missing and input ends before a valid one is entered | The `Run` ends with an error naming the key; nothing was changed |
| `git` or `curl` is missing | The `Run` ends with an error naming the tool; nothing was changed | | `git` or `curl` is missing | The `Run` ends with an error naming the tool; nothing was changed |
## Contract: provideProjectDetails ## Contract: provideProjectDetails
| Item | Value | | Item | Value |
| --- | --- | | --- | --- |
| Operation | `provideProjectDetails(name: String, description: String, visibility: Visibility, giteaOwner: Owner, githubOwner: Owner [0..1], directory: Path, enablePlanGate: Boolean): Summary` | | Operation | `provideProjectDetails(name: String, description: String, visibility: Visibility, giteaOwner: Owner, githubOwner: Owner [0..1], directory: Path, enablePlanGate: Boolean, writeEnvFile: Boolean): Summary` |
| Traces to | `provideProjectDetails` in [SSD-001] | | Traces to | `provideProjectDetails` in [SSD-001] |
| Concepts | ProjectRequest, PreflightResult, GiteaRepository, GitHubRepository, LicenseFile, PushMirror, LocalProject, Remote, Submodule, HookSetup, Summary | | Concepts | ProjectRequest, PreflightResult, GiteaRepository, GitHubRepository, LicenseFile, PushMirror, LocalProject, Remote, Submodule, HookSetup, EnvFile, Summary |
**Preconditions** **Preconditions**
- A `Run` exists and its `Configuration` is valid (from `startProjectCreation`). - A `Run` exists and its `Configuration` is valid (from `startProjectCreation`).
- `githubOwner` is present exactly when the Maintainer chose GitHub. - `githubOwner` is present exactly when the Maintainer chose GitHub.
- The license that applies is not asked: it comes from the `Configuration` (`PROJECT_LICENSE`) or follows the GitHub choice.
- When `githubOwner` is present, the GitHub `Credential`s are known: from `.env`, or entered by the Maintainer without echo and validated before the first request.
- A detail that the `Configuration` defines is not asked: it is taken from the `Configuration`. - A detail that the `Configuration` defines is not asked: it is taken from the `Configuration`.
**Postconditions** **Postconditions**
- P1. A `ProjectRequest` instance was created with the attributes given by the Maintainer or defined by the `Configuration`, and associated with the `Run`. - P1. A `ProjectRequest` instance was created with the attributes given by the Maintainer or defined by the `Configuration`, and associated with the `Run`.
- P2. A `PreflightResult` instance was created and associated with the `ProjectRequest`, recording that each token needed for the chosen hosts works, that each owner accepts new repositories, that the name is free on the chosen hosts, that `AGPL-3.0` is offered by Gitea when GitHub was chosen, and the outcome of the SSH test to Gitea on port 10022. - P2. A `PreflightResult` instance was created and associated with the `ProjectRequest`, recording that each token needed for the chosen hosts works, that each owner accepts new repositories, that the name is free on the chosen hosts, that the license that applies is offered by Gitea (when a license applies), and the outcome of the SSH test to Gitea on port 10022.
- P3. A `GiteaRepository` instance was created under `giteaOwner` with the given name, description and visibility, and associated with the `ProjectRequest`. - P3. A `GiteaRepository` instance was created under `giteaOwner` with the given name, description and visibility, and associated with the `ProjectRequest`.
- P4. If `githubOwner` is present, a `LicenseFile` instance for `AGPL-3.0` was created and associated with the `GiteaRepository`, so that repository is not empty. Otherwise the `GiteaRepository` has no `LicenseFile` and is empty. - P4. If a license applies, a `LicenseFile` instance for it was created and associated with the `GiteaRepository`, so that repository is not empty. The license that applies is the one the `Configuration` defines (`PROJECT_LICENSE`; `none` means none), otherwise `AGPL-3.0` if `githubOwner` is present and `visibility` is `public`, otherwise none. If no license applies the `GiteaRepository` has no `LicenseFile` and is empty.
- P5. If `githubOwner` is present, an empty `GitHubRepository` instance was created under `githubOwner` and associated with the `ProjectRequest`. - P5. If `githubOwner` is present, an empty `GitHubRepository` instance was created under `githubOwner` and associated with the `ProjectRequest`.
- P6. If `githubOwner` is present, a `PushMirror` instance was created, associated with the `GiteaRepository` as source and the `GitHubRepository` as target, with its effective sync setting recorded, and a first sync was requested. - P6. If `githubOwner` is present, a `PushMirror` instance was created, associated with the `GiteaRepository` as source and the `GitHubRepository` as target, with its effective sync setting recorded, and a first sync was requested.
- P7. A `LocalProject` instance was created at `directory`, associated with the `ProjectRequest`. If the `GiteaRepository` is not empty, the `LocalProject` holds its history, including the `LicenseFile` commit. - P7. A `LocalProject` instance was created at `directory`, associated with the `ProjectRequest`. If the `GiteaRepository` is not empty, the `LocalProject` holds its history, including the `LicenseFile` commit.
- P8. A `Remote` named `origin` was associated with the `LocalProject`, pointing at the `GiteaRepository` over SSH if the SSH test passed, otherwise over HTTPS, with no credential in its URL. - P8. A `Remote` named `origin` was associated with the `LocalProject`, pointing at the `GiteaRepository` over SSH if the SSH test passed, otherwise over HTTPS, with no credential in its URL.
- P9. If `githubOwner` is present, a `Remote` named `github` was associated with the `LocalProject`, pointing at the `GitHubRepository`, with no credential in its URL. - P9. If `githubOwner` is present, a `Remote` named `github` was associated with the `LocalProject`, pointing at the `GitHubRepository`, with no credential in its URL.
- P10. A `Submodule` named `framework` was associated with the `LocalProject`. - P10. A `Submodule` named `framework` was associated with the `LocalProject`, and the submodules the framework itself holds (`qc`) were initialised.
- P11. A `HookSetup` instance was associated with the `LocalProject`, recording that skills and git hooks were installed once and, if `enablePlanGate`, that the plan gate was enabled. - P11. A `HookSetup` instance was associated with the `LocalProject`, recording that skills and git hooks were installed once and, if `enablePlanGate`, that the plan gate was enabled.
- P12. `AGENTS.md` and `docs/artifact-registry.md` exist in the `LocalProject`, each either newly copied from the framework templates or left as it was because the Maintainer declined to replace it. - P12. `AGENTS.md` and `docs/artifact-registry.md` exist in the `LocalProject`, each either newly copied from the framework templates or left as it was because the Maintainer declined to replace it.
- P13. A `Summary` instance was created listing every created item, every skipped item and the next step for anything that failed, and is returned. It contains no credential. - P13. A `Summary` instance was created listing every created item, every skipped item and the next step for anything that failed, and is returned. It contains no credential.
- P14. If `writeEnvFile`, an `EnvFile` named `.env` was associated with the `LocalProject`, holding only the `Credential`s the project needs (the Gitea token, and the GitHub token and account name when `githubOwner` is present). It is readable by its owner only and excluded from git without a change to any tracked file, and no `Credential` is shown in any output. If `writeEnvFile` is false, no `EnvFile` was created. An existing `.env` is left as it was unless the Maintainer agreed to replace it.
**Exceptions** **Exceptions**
| Condition (failing precondition) | Outcome | | Condition (failing precondition) | Outcome |
| --- | --- | | --- | --- |
| A token is invalid, an owner refuses new repositories, or the name is taken on a chosen host (P2) | The `Run` ends before P3; nothing was created; the error names the failed check | | A token is invalid, an owner refuses new repositories, or the name is taken on a chosen host (P2) | The `Run` ends before P3; nothing was created; the error names the failed check |
| GitHub was chosen and Gitea does not offer `AGPL-3.0` (P2) | The `Run` ends before P3; nothing was created | | A license applies and Gitea does not offer it (P2) | The `Run` ends before P3; nothing was created; the error names the license |
| `GiteaRepository` creation fails after a `GitHubRepository` was created (P5, P3 ordering) | The `Summary` lists the `GitHubRepository` as created, the `GiteaRepository` as failed and how to continue | | `GiteaRepository` creation fails after a `GitHubRepository` was created (P5, P3 ordering) | The `Summary` lists the `GitHubRepository` as created, the `GiteaRepository` as failed and how to continue |
| `PushMirror` creation fails (P6) | The `Summary` lists both repositories as created, the mirror as failed and how to continue; the local steps are not run | | `PushMirror` creation fails (P6) | The `Summary` lists both repositories as created, the mirror as failed and how to continue; the local steps are not run |
| `directory` exists, or a target file exists, and the Maintainer declines replacing it (P7, P12) | That item is skipped and listed in the `Summary` | | `directory` exists, or a target file exists, and the Maintainer declines replacing it (P7, P12) | That item is skipped and listed in the `Summary` |
| A `.env` already exists in the `LocalProject` and the Maintainer declines replacing it (P14) | That item is skipped and listed in the `Summary` |
| A different `core.hooksPath` exists and the Maintainer declines replacing it (P11) | Hooks are not installed and this is listed in the `Summary` | | A different `core.hooksPath` exists and the Maintainer declines replacing it (P11) | Hooks are not installed and this is listed in the `Summary` |
| The framework's own submodules cannot be fetched (P10) | The `Summary` lists the `framework` `Submodule` as added, its own submodules as failed, and the command `git submodule update --init --recursive` to run by hand |
| SSH to port 10022 fails and the `Submodule` cannot be added (P10) | The `Summary` lists the repositories as created, the submodule as failed, and the SSH prerequisite | | SSH to port 10022 fails and the `Submodule` cannot be added (P10) | The `Summary` lists the repositories as created, the submodule as failed, and the SSH prerequisite |
--- ---
@@ -90,5 +96,5 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
[DM-001]: ./dm.md [DM-001]: ./dm.md
[DICT-001]: ../dictionary.md [DICT-001]: ../dictionary.md
[SD-001]: ./sd.md [SD-001]: ./sd.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+56 -28
View File
@@ -4,17 +4,17 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | SD-001 | | ID | SD-001 |
| CrossReference | [OC-001] | | CrossReference | [OC-001], [DCD-001] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] | | 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | The license that applies is passed to hasLicense and createRepository; no alt on the GitHub choice | [d773fa9] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Note: preset project details are read by ConfigLoader | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | P4: the license passed is AGPL-3.0 only for GitHub with a public project | [1cd27f7] |
--- ---
Design objects are conceptual; in `create-project.sh` each becomes a small function group. No Design Class Diagram exists yet. Design objects are conceptual; in `create-project.sh` each becomes a small function group. [DCD-001] gives each object its class and turns each message below into a method signature.
## Sequence: startProjectCreation ## Sequence: startProjectCreation
@@ -28,6 +28,7 @@ actor Maintainer
participant ":ProjectCreator" as PC participant ":ProjectCreator" as PC
participant ":ConfigLoader" as CL participant ":ConfigLoader" as CL
participant ":ToolChecker" as TC participant ":ToolChecker" as TC
participant ":CredentialCollector" as CC
Maintainer -> PC : startProjectCreation() Maintainer -> PC : startProjectCreation()
activate PC activate PC
@@ -36,6 +37,11 @@ PC -> CL : load(config.env, .env)
activate CL activate CL
CL --> PC : configuration CL --> PC : configuration
deactivate CL deactivate CL
create CC
PC -> CC : collect(configuration, GITEA_TOKEN)
activate CC
CC --> PC : configuration
deactivate CC
create TC create TC
PC -> TC : check(git, curl, jq) PC -> TC : check(git, curl, jq)
activate TC activate TC
@@ -44,6 +50,7 @@ deactivate TC
PC --> Maintainer : promptSet PC --> Maintainer : promptSet
deactivate PC deactivate PC
destroy CL destroy CL
destroy CC
destroy TC destroy TC
@enduml @enduml
``` ```
@@ -53,7 +60,7 @@ destroy TC
| Pattern (GRASP / GoF) | Applied to | Rationale | | Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- | | --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | Receives the system operations and coordinates, without doing the work itself | | Controller (GRASP) | `ProjectCreator` | Receives the system operations and coordinates, without doing the work itself |
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker` | No domain concept owns parsing or tool checks; separate small units keep cohesion high | | Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker`, `CredentialCollector` | No domain concept owns parsing, tool checks or asking for a credential; separate small units keep cohesion high |
| Creator (GRASP) | `ConfigLoader` creates `Configuration` | It holds the data needed to build and validate it | | Creator (GRASP) | `ConfigLoader` creates `Configuration` | It holds the data needed to build and validate it |
### Postcondition Coverage ### Postcondition Coverage
@@ -61,13 +68,13 @@ destroy TC
| Postcondition (from contract) | Satisfied by message | | Postcondition (from contract) | Satisfied by message |
| --- | --- | | --- | --- |
| P1 Run created | `startProjectCreation` received by `ProjectCreator` | | P1 Run created | `startProjectCreation` received by `ProjectCreator` |
| P2 Configuration created and validated | `load(config.env, .env)` | | P2 Configuration created and validated; a missing credential entered, validated and held in memory | `load(config.env, .env)` and `collect(configuration, GITEA_TOKEN)` |
| P3 ToolCheck created | `check(git, curl, jq)` | | P3 ToolCheck created | `check(git, curl, jq)` |
| P4 PromptSet returned | `promptSet` return to the Maintainer | | P4 PromptSet returned | `promptSet` return to the Maintainer |
### Responsibility Check ### Responsibility Check
`ProjectCreator` only sequences two calls; parsing and validation sit in `ConfigLoader`, tool detection in `ToolChecker`. No object receives every message. Project details preset in `config.env` are read and validated by `ConfigLoader` as part of `configuration`; the second sequence is unchanged, because `provideProjectDetails` receives the same arguments whether they were asked or preset. `ProjectCreator` only sequences three calls; parsing and validation sit in `ConfigLoader`, asking for a missing credential in `CredentialCollector`, tool detection in `ToolChecker`. No object receives every message. Project details preset in `config.env` are read and validated by `ConfigLoader` as part of `configuration`; the second sequence is unchanged, because `provideProjectDetails` receives the same arguments whether they were asked or preset.
## Sequence: provideProjectDetails ## Sequence: provideProjectDetails
@@ -85,8 +92,10 @@ participant ":GitHubClient" as GH
participant ":LocalProjectBuilder" as LB participant ":LocalProjectBuilder" as LB
participant ":FrameworkInstaller" as FI participant ":FrameworkInstaller" as FI
participant ":SummaryReport" as SR participant ":SummaryReport" as SR
participant ":CredentialCollector" as CC
participant ":EnvFileWriter" as EW
Maintainer -> PC : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate) Maintainer -> PC : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile)
activate PC activate PC
create GT create GT
PC -> GT : new(configuration) PC -> GT : new(configuration)
@@ -94,10 +103,20 @@ opt githubOwner present
create GH create GH
PC -> GH : new(configuration) PC -> GH : new(configuration)
end end
opt githubOwner present
create CC
PC -> CC : collect(configuration, GITHUB_PAT, GITHUB_USER)
activate CC
CC --> PC : configuration
deactivate CC
end
create PF create PF
PC -> PF : check(request) PC -> PF : check(request)
activate PF activate PF
PF -> GT : verifyToken(), ownerAccepts(giteaOwner), nameFree(name), hasLicense(AGPL-3.0) PF -> GT : verifyToken(), ownerAccepts(giteaOwner), nameFree(name)
opt a license applies
PF -> GT : hasLicense(license)
end
opt githubOwner present opt githubOwner present
PF -> GH : verifyToken(), ownerAccepts(githubOwner), nameFree(name) PF -> GH : verifyToken(), ownerAccepts(githubOwner), nameFree(name)
end end
@@ -105,17 +124,13 @@ PF --> PC : preflightResult
deactivate PF deactivate PF
opt githubOwner present opt githubOwner present
PC -> GH : createEmptyRepository(githubOwner, name) PC -> GH : createEmptyRepository(request)
activate GH activate GH
GH --> PC : gitHubRepository GH --> PC : gitHubRepository
deactivate GH deactivate GH
end end
alt githubOwner present PC -> GT : createRepository(request, license)
PC -> GT : createRepository(giteaOwner, name, license=AGPL-3.0)
else no GitHub
PC -> GT : createRepository(giteaOwner, name, license=none)
end
activate GT activate GT
GT --> PC : giteaRepository GT --> PC : giteaRepository
deactivate GT deactivate GT
@@ -123,7 +138,7 @@ deactivate GT
opt githubOwner present opt githubOwner present
PC -> GT : addPushMirror(giteaRepository, gitHubRepository) PC -> GT : addPushMirror(giteaRepository, gitHubRepository)
activate GT activate GT
GT -> GT : requestSync() GT -> GT : requestSync(pushMirror)
GT --> PC : pushMirror GT --> PC : pushMirror
deactivate GT deactivate GT
end end
@@ -137,11 +152,19 @@ deactivate LB
create FI create FI
PC -> FI : install(localProject, enablePlanGate) PC -> FI : install(localProject, enablePlanGate)
activate FI activate FI
FI --> PC : submodule, hookSetup, templates FI --> PC : installResult
deactivate FI deactivate FI
opt writeEnvFile
create EW
PC -> EW : write(localProject, configuration, githubOwner present)
activate EW
EW --> PC : envFile
deactivate EW
end
create SR create SR
PC -> SR : compose(all results) PC -> SR : compose(request)
SR --> PC : summary SR --> PC : summary
PC --> Maintainer : summary PC --> Maintainer : summary
deactivate PC deactivate PC
@@ -150,6 +173,8 @@ destroy GT
destroy GH destroy GH
destroy LB destroy LB
destroy FI destroy FI
destroy CC
destroy EW
destroy SR destroy SR
@enduml @enduml
``` ```
@@ -159,7 +184,7 @@ destroy SR
| Pattern (GRASP / GoF) | Applied to | Rationale | | Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- | | --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | Single entry for the system operation; sequences the steps and stops on the first failure | | Controller (GRASP) | `ProjectCreator` | Single entry for the system operation; sequences the steps and stops on the first failure |
| Pure Fabrication (GRASP) | `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport` | Each groups one responsibility that no domain concept owns | | Pure Fabrication (GRASP) | `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport`, `CredentialCollector`, `EnvFileWriter` | Each groups one responsibility that no domain concept owns |
| Facade (GoF) | `GiteaClient`, `GitHubClient` | Hide each host's HTTP API and credential handling behind a small interface; tokens never leave them | | Facade (GoF) | `GiteaClient`, `GitHubClient` | Hide each host's HTTP API and credential handling behind a small interface; tokens never leave them |
| Protection from variations (GRASP) | Client classes | The `github`-optional and license variations are decided by the controller's `alt` and `opt`, not inside the clients | | Protection from variations (GRASP) | Client classes | The `github`-optional and license variations are decided by the controller's `alt` and `opt`, not inside the clients |
@@ -168,25 +193,28 @@ destroy SR
| Postcondition (from contract) | Satisfied by message | | Postcondition (from contract) | Satisfied by message |
| --- | --- | | --- | --- |
| P1 ProjectRequest created | `provideProjectDetails` received by `ProjectCreator` | | P1 ProjectRequest created | `provideProjectDetails` received by `ProjectCreator` |
| P2 PreflightResult created | `check(request)` | | P2 PreflightResult created, including that the license is offered | `check(request)` and `hasLicense(license)` |
| P3 GiteaRepository created | `createRepository(giteaOwner, name, license)` | | P3 GiteaRepository created | `createRepository(request, license)` |
| P4 LicenseFile when GitHub chosen, otherwise empty | `createRepository(..., license=AGPL-3.0)` and the `alt` branch `license=none` | | P4 LicenseFile when a license applies, otherwise empty | `createRepository(request, license)`; `license` is the one the `ProjectRequest` carries (`PROJECT_LICENSE`, or AGPL-3.0 when GitHub was chosen and the project is public), and is absent for `none` |
| P5 empty GitHubRepository when chosen | `createEmptyRepository(githubOwner, name)` | | P2 GitHub credentials known before the first request | `collect(configuration, GITHUB_PAT, GITHUB_USER)` inside `opt githubOwner present` |
| P6 PushMirror and first sync | `addPushMirror(...)` and `requestSync()` | | P5 empty GitHubRepository when chosen | `createEmptyRepository(request)` |
| P6 PushMirror and first sync | `addPushMirror(...)` and `requestSync(pushMirror)` |
| P7 LocalProject created, history from Gitea when not empty | `build(directory, ...)` | | P7 LocalProject created, history from Gitea when not empty | `build(directory, ...)` |
| P8 origin remote (SSH if the test passed, else HTTPS) | `build(..., sshPassed)` | | P8 origin remote (SSH if the test passed, else HTTPS) | `build(..., sshPassed)` |
| P9 github remote when chosen | `build(...)` | | P9 github remote when chosen | `build(...)` |
| P10 framework Submodule | `install(localProject, ...)` | | P10 framework Submodule | `install(localProject, ...)` |
| P11 HookSetup, plan gate if chosen | `install(localProject, enablePlanGate)` | | P11 HookSetup, plan gate if chosen | `install(localProject, enablePlanGate)` |
| P12 AGENTS.md and registry copied or kept | `install(...)` returning `templates` | | P12 AGENTS.md and registry copied or kept | `install(...)` returning `templates` |
| P13 Summary created and returned | `compose(all results)` and the final return | | P13 Summary created and returned | `compose(request)` and the final return |
| P14 EnvFile created with the needed credentials, owner-only, ignored by git, or none when declined | `write(localProject, configuration, githubOwner present)` inside `opt writeEnvFile` |
### Responsibility Check ### Responsibility Check
`ProjectCreator` sequences and decides on the optional paths but performs no HTTP, git or file work. Host calls are in the two clients, local work in `LocalProjectBuilder` and `FrameworkInstaller`, reporting in `SummaryReport`, so cohesion stays high and no object receives all messages. Failure handling (exceptions in [OC-001]) is the controller's single stop-and-report rule and is not drawn. `ProjectCreator` sequences and decides on the optional paths but performs no HTTP, git or file work. Host calls are in the two clients, local work in `LocalProjectBuilder` and `FrameworkInstaller`, the credential prompts in `CredentialCollector`, the `.env` in `EnvFileWriter`, reporting in `SummaryReport`, so cohesion stays high and no object receives all messages. Failure handling (exceptions in [OC-001]) is the controller's single stop-and-report rule and is not drawn.
--- ---
[OC-001]: ./oc.md [OC-001]: ./oc.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [DCD-001]: ./dcd.md
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+10 -8
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited OC-001 and DM-001 | [02875ae] | | 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | The license is not a parameter: it comes from config.env or follows the GitHub choice | [d773fa9] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Parameters may come from config.env; the message is unchanged | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | The license that follows the GitHub choice also needs a public project | [1cd27f7] |
--- ---
@@ -26,7 +26,7 @@ actor Maintainer as A
participant ":System" as S participant ":System" as S
A -> S : startProjectCreation() A -> S : startProjectCreation()
S --> A : prompts for project details S --> A : prompts for project details
A -> S : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate) A -> S : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile)
S --> A : checks passed S --> A : checks passed
S --> A : creation summary S --> A : creation summary
@enduml @enduml
@@ -36,19 +36,21 @@ S --> A : creation summary
| Step | Message | Parameters | Return | Use case step | | Step | Message | Parameters | Return | Use case step |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| 1 | startProjectCreation | none | prompts for project details (after configuration and tool checks) | 1, 2 | | 1 | startProjectCreation | none | prompts for project details (after configuration and tool checks; a missing credential is asked first) | 1, 2 |
| 2 | provideProjectDetails | name, description, visibility, giteaOwner, githubOwner (optional; given means GitHub is chosen and the Gitea repository gets the AGPL license; omitted means no GitHub and no license), directory, enablePlanGate (each of these may come from `config.env` instead of the Maintainer; the message is unchanged) | checks passed, then a creation summary | 3 to 10 | | 2 | provideProjectDetails | name, description, visibility, giteaOwner, githubOwner (optional; given means GitHub is chosen; omitted means no GitHub), directory, enablePlanGate (each of these may come from `config.env` instead of the Maintainer; the message is unchanged), writeEnvFile (whether to create the project's `.env`), and the credentials that `.env` does not provide (GitHub ones only when GitHub is chosen) | checks passed, then a creation summary | 3 to 10 |
The license that applies is not a parameter: it is read from `config.env` (`PROJECT_LICENSE`) or, when none is set, follows the GitHub choice and the project's visibility (AGPL-3.0 only for GitHub with a public project).
Steps 4 to 9 are internal to the system, so one operation covers them. A consent question (step 8a, 9a, 9b) is a prompt from the system and is out of scope for this diagram; failure flows are out of scope here. Steps 4 to 9 are internal to the system, so one operation covers them. A consent question (step 8a, 9a, 9b) is a prompt from the system and is out of scope for this diagram; failure flows are out of scope here.
## Lifecycle Notes ## Lifecycle Notes
The system is one script run. It starts with the first operation and ends after the summary; nothing persists between runs. The system is one script run. It starts with the first operation and ends after the summary; nothing persists between runs except the `.env` the Maintainer agreed to in the new project.
--- ---
[UC-001]: ./uc.md [UC-001]: ./uc.md
[DM-001]: ./dm.md [DM-001]: ./dm.md
[OC-001]: ./oc.md [OC-001]: ./oc.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+26 -15
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited DM-001 and UCD-001 | [02875ae] | | 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | The license comes from PROJECT_LICENSE (step 6, extension 4c); AGPL-3.0 is only the default when GitHub is chosen | [d773fa9] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Step 3: details set in config.env are not asked (extensions 3a, 3b) | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | License rule: the AGPL-3.0 default needs GitHub and a public project (step 6, rule 6); step 9 and extension 9e fetch the framework's own submodules | [1cd27f7] |
--- ---
@@ -26,40 +26,43 @@
- S02 — credentials are never exposed and nothing is overwritten silently - S02 — credentials are never exposed and nothing is overwritten silently
- S03 — the published procedure is documented and reusable - S03 — the published procedure is documented and reusable
- **Preconditions:** - **Preconditions:**
- `config.env` and `.env` exist and are valid. - `config.env` exists and is valid. `.env` may be missing or hold only some credentials; a credential it does not provide is asked.
- `config.env` may preset any of the project details of step 3. - `config.env` may preset any of the project details of step 3, and may set the project license (`PROJECT_LICENSE`).
- `git` and `curl` are installed. - `git` and `curl` are installed.
- The Maintainer has a Gitea token, a GitHub PAT (only when GitHub is chosen) and SSH access to Gitea on port 10022. - The Maintainer has a Gitea token, a GitHub PAT and a GitHub account name (only when GitHub is chosen) and SSH access to Gitea on port 10022.
- **Postconditions (success guarantee):** - **Postconditions (success guarantee):**
- A repository exists on Gitea under the chosen owner. It is empty, or, when the Maintainer chose GitHub, it holds the AGPL license file. - A repository exists on Gitea under the chosen owner. It is empty, or it holds the license file that applies: the license set in `config.env`, or AGPL-3.0 when the Maintainer chose GitHub, the project is public and no license is set.
- When the Maintainer chose to create a GitHub repository, an empty repository exists on GitHub under the chosen owner, the Gitea repository is a push mirror to it, and the AGPL license file reaches GitHub through the mirror. - When the Maintainer chose to create a GitHub repository, an empty repository exists on GitHub under the chosen owner, the Gitea repository is a push mirror to it, and the license file, if any, reaches GitHub through the mirror.
- A local project directory exists with credential-free remotes `origin` (Gitea) and, when GitHub was chosen, `github`, the `framework` submodule, installed skills and hooks, and the copied templates. - A local project directory exists with credential-free remotes `origin` (Gitea) and, when GitHub was chosen, `github`, the `framework` submodule, installed skills and hooks, and the copied templates.
- When the Maintainer agreed, the local project has a `.env` that holds only the credentials the project needs, is readable by its owner only and is ignored by git.
- The Maintainer has a summary of what was created. - The Maintainer has a summary of what was created.
### Main Success Scenario ### Main Success Scenario
1. The Maintainer starts the project creation. 1. The Maintainer starts the project creation.
2. The system loads and validates the configuration and credentials and checks that the required tools exist. 2. The system loads and validates the configuration and credentials and checks that the required tools exist. A credential that `.env` does not provide is asked, without echo; the GitHub credentials are asked once GitHub is chosen.
3. The Maintainer provides the repository name, description, visibility, the Gitea owner, whether to also create a GitHub repository (and if so its owner), the local directory, and whether to enable the plan gate. A detail that is set in `config.env` is not asked. 3. The Maintainer provides the repository name, description, visibility, the Gitea owner, whether to also create a GitHub repository (and if so its owner), the local directory, and whether to enable the plan gate. A detail that is set in `config.env` is not asked.
4. The system checks that the tokens needed for the chosen hosts work, that the owners accept new repositories, that the name is free on those hosts, and whether SSH to Gitea works. 4. The system checks that the tokens needed for the chosen hosts work, that the owners accept new repositories, that the name is free on those hosts, that Gitea offers the license that applies (if any), and whether SSH to Gitea works.
5. Optional: if the Maintainer chose GitHub, the system creates the empty GitHub repository. 5. Optional: if the Maintainer chose GitHub, the system creates the empty GitHub repository.
6. The system creates the Gitea repository. If the Maintainer chose GitHub, the repository is created with the AGPL license file and so is not empty; otherwise it is empty and has no license. 6. The system creates the Gitea repository. If a license applies, the repository is created with its license file and so is not empty; otherwise it is empty. The license that applies is the one set in `config.env` (`PROJECT_LICENSE`; `none` means no license); when none is set it is AGPL-3.0 if the Maintainer chose GitHub and the project is public, and none otherwise. The license is never asked.
7. Optional: if GitHub was chosen, the system configures the Gitea repository as a push mirror to GitHub and verifies it. A license file in the Gitea repository is pushed to GitHub by the mirror. 7. Optional: if GitHub was chosen, the system configures the Gitea repository as a push mirror to GitHub and verifies it. A license file in the Gitea repository is pushed to GitHub by the mirror.
8. The system creates the local project with the `origin` remote and, if GitHub was chosen, the `github` remote. 8. The system creates the local project with the `origin` remote and, if GitHub was chosen, the `github` remote.
9. The system adds the framework submodule, installs its skills and hooks (and the plan gate if chosen) and copies the templates. 9. The system adds the framework submodule and fetches its own submodules (the `qc` checklists), installs its skills and hooks (and the plan gate if chosen) and copies the templates. If the Maintainer agrees, it also creates the project's own `.env` with the credentials the project needs.
10. The system reports a summary of what was created. 10. The system reports a summary of what was created.
### Extensions (Alternative / Exception Flows) ### Extensions (Alternative / Exception Flows)
- 2a. A required tool is missing, or a configuration value is missing or malformed: - 2a. A required tool is missing, or a configuration value is missing or malformed:
1. The system stops before any change and names the problem without showing a credential. 1. The system stops before any change and names the problem without showing a credential.
- 2b. A credential is not provided in `.env`:
1. The system asks for it without showing what is typed. An invalid value is refused and asked again; when input ends the system stops before any change and names the key.
- 3a. A project detail is set in `config.env`: - 3a. A project detail is set in `config.env`:
1. The system uses it and does not ask for it; the summary says it came from the configuration. 1. The system uses it and does not ask for it; the summary says it came from the configuration.
- 3b. A configured project detail is invalid: - 3b. A configured project detail is invalid:
1. The system stops before any request and names the key; it does not ask for the value instead. 1. The system stops before any request and names the key; it does not ask for the value instead.
- 4a. A token is invalid, an owner does not accept the repository, or the name is taken: - 4a. A token is invalid, an owner does not accept the repository, or the name is taken:
1. The system stops before creating anything and says which check failed. The GitHub token is only checked when GitHub was chosen. 1. The system stops before creating anything and says which check failed. The GitHub token is only checked when GitHub was chosen.
- 4c. GitHub was chosen and the Gitea server does not offer the `AGPL-3.0` license: - 4c. A license applies and the Gitea server does not offer it:
1. The system stops before creating anything and names the missing license. 1. The system stops before creating anything and names the missing license.
- 4b. SSH to Gitea does not work: - 4b. SSH to Gitea does not work:
1. The system uses HTTPS for `origin` and warns that the framework submodule step will fail until SSH is configured. 1. The system uses HTTPS for `origin` and warns that the framework submodule step will fail until SSH is configured.
@@ -69,16 +72,24 @@
1. The system asks the Maintainer before replacing it; on no, it skips that item and reports it. 1. The system asks the Maintainer before replacing it; on no, it skips that item and reports it.
- 9b. A different git hooks setup is already configured in the project: - 9b. A different git hooks setup is already configured in the project:
1. The system asks before replacing it. 1. The system asks before replacing it.
- 9c. The Maintainer declines creating the project's `.env`:
1. The system creates none and says so in the summary.
- 9d. A `.env` already exists in the project:
1. The system asks before replacing it; on no, it keeps it and reports it.
- 9e. The framework's own submodules cannot be fetched:
1. The system stops the step, reports what exists and names the command to run by hand, `git submodule update --init --recursive`, without showing a credential.
### Special Requirements / Business Rules ### Special Requirements / Business Rules
| Step | Rule | | Step | Rule |
| --- | --- | | --- | --- |
| 2, 4 | A token never appears in output, logs, command lines, remote URLs or temporary files left behind | | 2, 4 | A token never appears in output, logs, command lines, remote URLs or temporary files left behind |
| 2 | A credential that is asked is read without echo, validated like one read from `.env`, and held in memory for the run |
| 9 | The project's `.env` is the only place a token is written. It is created only after a yes (default no), holds only the keys the project needs (`GITEA_TOKEN`; `GITHUB_PAT` and `GITHUB_USER` when GitHub was chosen), is readable by its owner only, is excluded from git without changing a tracked file, and is never replaced without a yes |
| 3 | The GitHub owner and the Gitea owner are chosen separately; `GITHUB_USER` is only the authenticating account | | 3 | The GitHub owner and the Gitea owner are chosen separately; `GITHUB_USER` is only the authenticating account |
| 3 | A project detail set in `config.env` (the key is present, even if empty where an empty value is allowed) is not asked; only the confirmations stay interactive | | 3 | A project detail set in `config.env` (the key is present, even if empty where an empty value is allowed) is not asked; only the confirmations stay interactive |
| 3, 5, 7 | GitHub is optional; without it no GitHub repository, mirror or `github` remote is created and the GitHub credentials are not required | | 3, 5, 7 | GitHub is optional; without it no GitHub repository, mirror or `github` remote is created and the GitHub credentials are not required |
| 6 | Choosing GitHub applies the AGPL license (key `AGPL-3.0`) to the Gitea repository when it is created, so that repository is not empty; without GitHub there is no license and the repository is empty | | 6 | The license that applies is added to the Gitea repository when it is created, so that repository is not empty: `PROJECT_LICENSE` if set (a Gitea license key such as `MIT`, or `none`), otherwise AGPL-3.0 when GitHub is chosen and the project is public, otherwise none. It is independent of the GitHub choice when set, and it is never asked |
| 7 | The mirror direction is Gitea to GitHub; the GitHub repository stays empty and receives its content from the mirror | | 7 | The mirror direction is Gitea to GitHub; the GitHub repository stays empty and receives its content from the mirror |
| 8 | `origin` uses HTTPS derived from `GITEA_URL`, or SSH when the SSH test in step 4 passed; when the Gitea repository is not empty (GitHub chosen) the local project is created by fetching it, not by an unrelated `git init` history | | 8 | `origin` uses HTTPS derived from `GITEA_URL`, or SSH when the SSH test in step 4 passed; when the Gitea repository is not empty (GitHub chosen) the local project is created by fetching it, not by an unrelated `git init` history |
| 8, 9 | Nothing is overwritten or deleted without consent, and no commit is made | | 8, 9 | Nothing is overwritten or deleted without consent, and no commit is made |
@@ -93,5 +104,5 @@
[US-001]: ../user-stories.md [US-001]: ../user-stories.md
[SA-001]: ../stakeholder-analysis.md [SA-001]: ../stakeholder-analysis.md
[DM-001]: ./dm.md [DM-001]: ./dm.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+102
View File
@@ -0,0 +1,102 @@
# Design Class Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | DCD-003 |
| CrossReference | [DM-003], [SD-002], [DICT-001], [UC-002], [DCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Default configuration files: --config and --env, else ./config.env and ./.env in the working folder, else the checkout's | [0ab5006] |
---
## Purpose and Scope
Covers [UC-002]. Adds the classes that start the script through a command link. `ProjectCreator`, `Configuration` and `Run` are the classes of [DCD-001] and change only as stated under Method Traceability. The consolidated diagram is [DCD-002].
## Diagram
```plantuml
@startuml
class Launcher {
+resolveCheckout(invocation : Path) : Checkout
+currentFolder() : WorkingFolder
+locateConfigFiles(configPath : Path [0..1], envPath : Path [0..1], checkout : Checkout, workingFolder : WorkingFolder) : ConfigFiles
+startFromWorkingFolder(configPath : Path [0..1], envPath : Path [0..1]) : PromptSet
}
class Checkout {
-path : Path
+configFile() : Path
+envFile() : Path
}
class WorkingFolder {
-path : Path
+configFile() : Path [0..1]
+envFile() : Path [0..1]
}
class ConfigFiles {
-configFile : Path
-envFile : Path
}
class ProjectCreator {
+startProjectCreation(workingFolder : WorkingFolder, configFiles : ConfigFiles) : PromptSet
}
class Run
Launcher "1" --> "1" ProjectCreator : starts
Launcher ..> Checkout : creates
Launcher ..> WorkingFolder : creates
Launcher ..> ConfigFiles : creates
Run "1" *-- "1" ConfigFiles
Run "1" *-- "1" Checkout
Run "1" *-- "1" WorkingFolder
@enduml
```
## Class Table
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
| --- | --- | --- | --- | --- |
| `Launcher` | Command Link (the object that follows it) | Follows the command link to the checkout, takes the folder the Maintainer stands in, chooses the two configuration files, and starts the run. | none | `resolveCheckout`, `currentFolder`, `locateConfigFiles`, `startFromWorkingFolder` |
| `Checkout` | Checkout | Names the folder that holds the script's own files and the default `config.env` and `.env`. | `path` | `configFile`, `envFile` |
| `WorkingFolder` | Working Folder | Names the base of the default directory of the new project and the files it may hold. | `path` | `configFile`, `envFile` |
| `ConfigFiles` | none (system concept of [OC-002]) | Carries the two files chosen for the `Configuration`. | `configFile`, `envFile` | none |
The concept Command Link has no class: it is a link the Maintainer makes with the shell, and the system only follows it.
## Method Traceability
| Method signature | Operation Contract / SD message |
| --- | --- |
| `Launcher.startFromWorkingFolder(configPath, envPath) : PromptSet` | [SD-002] `startFromWorkingFolder`; P1, P6 |
| `Launcher.resolveCheckout(invocation) : Checkout` | [SD-002] `resolveCheckout(invocation)`; P2 |
| `Launcher.currentFolder() : WorkingFolder` | [SD-002] `currentFolder()`; P3 |
| `Launcher.locateConfigFiles(configPath, envPath, checkout, workingFolder) : ConfigFiles` | [SD-002] `locateConfigFiles(...)`; P5 |
| `WorkingFolder.configFile() : Path [0..1]`, `WorkingFolder.envFile() : Path [0..1]` | [SD-002] `locateConfigFiles`; P5 |
| `Checkout.configFile() : Path`, `Checkout.envFile() : Path` | [SD-002] `locateConfigFiles`; P5 |
| `ProjectCreator.startProjectCreation(workingFolder, configFiles) : PromptSet` | [SD-002] `startProjectCreation`; P4, P6, P7. Replaces the signature of [DCD-001] by adding `workingFolder` and `configFiles` |
## Pattern Annotations
| Pattern | Classes | Rationale |
| --- | --- | --- |
| Controller | `Launcher` | One class takes the system operation of [UC-002] |
| Information Expert | `Launcher`, `Checkout` | The launcher knows the invocation; the checkout knows where its files are |
## Dependency Check
`Launcher` depends on `ProjectCreator`, `Checkout`, `WorkingFolder` and `ConfigFiles`; none of them depends on `Launcher`, so no cycle is added.
---
[DM-003]: ./dm.md
[SD-002]: ./sd.md
[DICT-001]: ../dictionary.md
[UC-002]: ./uc.md
[DCD-001]: ../uc-001/dcd.md
[DCD-002]: ../dcd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+89
View File
@@ -0,0 +1,89 @@
# Domain Model
## Metadata
| Key | Value |
| --- | --- |
| ID | DM-003 |
| CrossReference | [UC-002], [UCD-001], [SSD-002], [DICT-001], [DM-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Default configuration files: --config and --env, else ./config.env and ./.env in the working folder, else the checkout's | [0ab5006] |
---
## Purpose and Scope
Covers [UC-002] "Start the script as a global command". The concepts come from the nouns of that use case and use the PO terms of [DICT-001]. The concepts `Maintainer`, `Configuration`, `Project` and `Local Project` are those of [DM-001] and are not redefined here. The project-level model that consolidates all use cases is [DM-002].
## Diagram
Concepts, attributes and associations only — no operations.
```plantuml
@startuml
class Maintainer {
name
}
class "Command Link" as CommandLink {
name
folder
}
class Checkout {
path
}
class "Working Folder" as WorkingFolder {
path
}
class Configuration {
preset project details
}
class "Local Project" as LocalProject {
path
}
Maintainer "1" --> "0..*" CommandLink : makes
CommandLink "0..*" --> "1" Checkout : leads to
Maintainer "1" --> "1" WorkingFolder : starts the script in
Checkout "1" --> "1" Configuration : holds by default
WorkingFolder "1" --> "0..1" Configuration : may hold
WorkingFolder "1" --> "0..*" LocalProject : is the base of
@enduml
```
## Concept Table
| Concept | Definition | Attributes | Source (use case / glossary) |
| --- | --- | --- | --- |
| Command Link | A name in a folder on the shell's search path that leads to the script in the checkout | name, folder | [UC-002] step 1 "command link" |
| Checkout | The folder that holds RepoFoundry: the script, its own files and by default `config.env` and `.env` | path | [UC-002] step 4 "checkout" |
| Working Folder | The folder in which the Maintainer starts the script, under which the new project is created by default, and which may hold its own `config.env` and `.env` | path | [UC-002] step 2 "working folder" |
## Association Table
| From | Association (reading direction) | To | Multiplicity |
| --- | --- | --- | --- |
| Maintainer | makes | Command Link | 1 to 0..* |
| Command Link | leads to | Checkout | 0..* to 1 |
| Maintainer | starts the script in | Working Folder | 1 to 1 |
| Checkout | holds by default | Configuration | 1 to 1 |
| Working Folder | may hold | Configuration | 1 to 0..1 |
| Working Folder | is the base of | Local Project | 1 to 0..* |
## Generalizations
| General | Specializations | Is-a justification |
| --- | --- | --- |
| None | - | - |
---
[UC-002]: ./uc.md
[UCD-001]: ../use-case-diagram.md
[SSD-002]: ./ssd.md
[DICT-001]: ../dictionary.md
[DM-001]: ../uc-001/dm.md
[DM-002]: ../domain-model.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+58
View File
@@ -0,0 +1,58 @@
# Operation Contract
## Metadata
| Key | Value |
| --- | --- |
| ID | OC-002 |
| CrossReference | [SSD-002], [DM-003], [DICT-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Default configuration files: --config and --env, else ./config.env and ./.env in the working folder, else the checkout's | [0ab5006] |
---
Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-003]. `Run` and `PromptSet` are the system concepts of [OC-001]; `ConfigFiles` is a system concept of this contract: the two files chosen for the `Configuration`. The operation `provideProjectDetails` is the one of [OC-001]; the only change is the base of its default `directory`, stated in P4.
## Contract: startFromWorkingFolder
| Item | Value |
| --- | --- |
| Operation | `startFromWorkingFolder(configPath: Path [0..1], envPath: Path [0..1]): PromptSet` |
| Traces to | `startFromWorkingFolder` in [SSD-002] |
| Concepts | Run, CommandLink, Checkout, WorkingFolder, ConfigFiles, Configuration |
**Preconditions**
- A `CommandLink` exists that leads to the script in a `Checkout`, or the Maintainer started the script by its path.
- The Maintainer is in a `WorkingFolder`.
**Postconditions**
- P1. A `Run` instance was created.
- P2. A `Checkout` instance was created and associated with the `Run`, with `path` set to the folder that holds the script's own files, reached through the `CommandLink`, however many links lie between them.
- P3. A `WorkingFolder` instance was created and associated with the `Run`, with `path` set to the folder in which the Maintainer started the script. It was not changed by following the `CommandLink`.
- P4. The default of `directory` in the `PromptSet` is `./<name>` under the `WorkingFolder`, never under the `Checkout`.
- P5. A `ConfigFiles` instance was created and associated with the `Run`. Its `configFile` is `configPath` when given, otherwise `config.env` in the `WorkingFolder` when it exists, otherwise `config.env` in the `Checkout`; its `envFile` is chosen in the same way from `envPath` and `.env`. Each file is chosen on its own. The paths are named in the output before any request to a host.
- P6. A `Configuration` instance was created and associated with the `Run` from the `ConfigFiles`; the validation of [OC-001] `startProjectCreation` P2 and P3 applies.
- P7. The `Run` was associated with a `PromptSet` that is returned.
**Exceptions**
| Condition (failing precondition) | Outcome |
| --- | --- |
| The `Checkout`'s own files are not found from the link target | The `Run` ends with an error naming the folder it looked in; nothing was changed |
| `config.env` or `.env` is not named and is in neither the `WorkingFolder` nor the `Checkout` (P5) | The `Run` ends with an error naming both places it looked in and the options `--config` and `--env`; nothing was changed |
| A chosen file is in the `WorkingFolder` and the Maintainer does not confirm it (P5) | The `Run` ends before any request to a host; nothing was changed |
| A value in `config.env` or `.env` is malformed (P6) | As in [OC-001] `startProjectCreation`: the error names the key, never its value; nothing was changed |
---
[SSD-002]: ./ssd.md
[DM-003]: ./dm.md
[DICT-001]: ../dictionary.md
[OC-001]: ../uc-001/oc.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+96
View File
@@ -0,0 +1,96 @@
# Sequence Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | SD-002 |
| CrossReference | [OC-002], [DCD-003], [DICT-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Default configuration files: --config and --env, else ./config.env and ./.env in the working folder, else the checkout's | [0ab5006] |
---
Design objects are conceptual; in `create-project.sh` each becomes a small function group. [DCD-003] gives each object its class and turns each message below into a method signature. `ProjectCreator` and `ConfigLoader` are the design objects of [SD-001].
## Sequence: startFromWorkingFolder
**Realizes:** `startFromWorkingFolder` in [OC-002]
### Diagram
```plantuml
@startuml
actor Maintainer
participant ":Launcher" as L
participant ":ProjectCreator" as PC
participant ":Checkout" as CK
participant ":WorkingFolder" as WF
participant ":ConfigFiles" as CF
Maintainer -> L : startFromWorkingFolder(configPath, envPath)
activate L
L -> L : resolveCheckout(invocation)
create CK
L -> CK : Checkout(path)
L -> L : currentFolder()
create WF
L -> WF : WorkingFolder(path)
L -> L : locateConfigFiles(configPath, envPath, checkout, workingFolder)
create CF
L -> CF : ConfigFiles(configFile, envFile)
alt a chosen file is in the working folder
L -> Maintainer : confirm the files and the Gitea address
Maintainer --> L : yes or no
end
alt the Checkout's own files, or both places for a config file, are not found
L --> Maintainer : error naming the folder looked in
end
L -> PC : startProjectCreation(workingFolder, configFiles)
activate PC
alt a value in config.env or .env is malformed
PC --> L : error naming the key
end
PC --> L : promptSet
deactivate PC
L --> Maintainer : promptSet
deactivate L
@enduml
```
### Pattern Annotations
| Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- |
| Information Expert | `Launcher.resolveCheckout`, `Launcher.locateConfigFiles` | The launcher knows how the script was invoked, so it is the one that can follow the command link |
| Controller | `Launcher` | One object takes the system operation and hands the work to `ProjectCreator`; `ProjectCreator` stays unaware of links |
| Low Coupling | `ProjectCreator` receives `workingFolder` and `configFiles` as values | The use case [UC-001] runs unchanged whatever way the script was started |
### Postcondition Coverage
| Postcondition (from contract) | Satisfied by message |
| --- | --- |
| P1 Run | `startFromWorkingFolder` (the run starts with it) |
| P2 Checkout | `resolveCheckout(invocation)` and the creation of `Checkout` |
| P3 WorkingFolder | `currentFolder()` and the creation of `WorkingFolder` |
| P4 default directory under the WorkingFolder | `startProjectCreation(workingFolder, configFiles)`; the prompt default is built from `workingFolder` |
| P5 ConfigFiles | `locateConfigFiles(...)` and the creation of `ConfigFiles`; the paths are named before any request |
| P6 Configuration | `startProjectCreation(workingFolder, configFiles)` loads the two files (`load` of [SD-001]) |
| P7 PromptSet | the returned `promptSet` |
| Exceptions: files not found, not confirmed, or malformed | the `alt` fragments |
### Responsibility Check
`Launcher` only finds the checkout, the working folder and the two files; it reads no configuration and makes no repository. `ProjectCreator` keeps every other responsibility of [SD-001].
---
[OC-002]: ./oc.md
[DCD-003]: ./dcd.md
[DICT-001]: ../dictionary.md
[SD-001]: ../uc-001/sd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+54
View File
@@ -0,0 +1,54 @@
# System Sequence Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | SSD-002 |
| CrossReference | [UC-002], [DM-003] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Names the confirmation of a configuration file from the working folder as out of scope | [0ab5006] |
---
## Source Use Case
Start the script as a global command ([UC-002]) — scenario: main success scenario
## Diagram
```plantuml
@startuml
actor Maintainer as A
participant ":System" as S
A -> S : startFromWorkingFolder(configPath, envPath)
S --> A : prompts for project details
A -> S : provideProjectDetails(...)
S --> A : creation summary with the project's full path
@enduml
```
## System Operations
| Step | Message | Parameters | Return | Use case step |
| --- | --- | --- | --- | --- |
| 3 to 4 | startFromWorkingFolder | configPath (optional), envPath (optional) | prompts for project details whose default directory is under the working folder | 3, 4, 5 |
| 5 | provideProjectDetails | the parameters of `provideProjectDetails` in [UC-001] | creation summary that names the full path of the new project | 5, 6 |
Failure flows (extensions 1a, 3a, 4a and 4b of [UC-002]) are out of scope for this diagram: they end the run with a message and add no system operation. The confirmation of a file from the working folder (extension 4c) is a prompt from the system, out of scope as the consent questions of [SSD-001] are. `provideProjectDetails` is the operation of [UC-001] and is not repeated in [OC-002]; the only difference is the base of the default `directory`. Making the command link (step 1) and opening the shell (step 2) are done by the Maintainer outside the system, so they are not system operations.
## Lifecycle Notes
One script run, as in [UC-001]. The command link persists between runs; the system keeps no state of it.
---
[UC-002]: ./uc.md
[DM-003]: ./dm.md
[OC-002]: ./oc.md
[SSD-001]: ../uc-001/ssd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+80
View File
@@ -0,0 +1,80 @@
# Start the script as a global command
## Metadata
| Key | Value |
| --- | --- |
| ID | UC-002 |
| CrossReference | [UCD-001], [US-001], [SA-001], [BC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Default configuration files: --config and --env, else ./config.env and ./.env in the working folder, else the checkout's; the files used are named and a file from the working folder is confirmed (step 4, extensions 4b and 4c, rules) | [0ab5006] |
---
**Format:** Fully Dressed
## Fully Dressed
- **Scope:** RepoFoundry (`create-project.sh`)
- **Level:** user-goal
- **Primary Actor:** Maintainer (S01 or S02; one person holds both roles for now)
- **Stakeholders and Interests:**
- S01 — the script is started by name from any folder, and the new project lands where S01 stands
- S02 — starting through a link never reads or writes outside the checkout and the current folder
- S03 — the README says exactly how to make the command global
- **Preconditions:**
- The checkout of RepoFoundry exists. `config.env` and `.env` exist as described in the README in the working folder or in the checkout, or the Maintainer points to them with `--config` and `--env`.
- A folder that is on the shell's `PATH` exists and the Maintainer may write to it.
- **Postconditions (success guarantee):**
- A command link exists in a `PATH` folder and leads to the script in the checkout.
- The Maintainer started the script by that name from a working folder, and use case [UC-001] ran with that working folder as its base: the default directory of the new project is `./<name>` under it.
- Nothing was written outside the working folder and the new project.
### Main Success Scenario
1. The Maintainer makes the script reachable by name: creates a command link in a `PATH` folder that leads to `src/create-project.sh` in the checkout (the README gives the command).
2. The Maintainer opens a shell in the folder in which the new project is to be created (the working folder).
3. The Maintainer starts the script by the name of the command link.
4. The system follows the command link to the checkout, loads its own files from there, and chooses `config.env` and `.env`, each one separately: the file named by `--config` or `--env`, otherwise the one in the working folder, otherwise the one in the checkout. It names the files it will use before any request to a host.
5. The system runs [UC-001] (`<<include>>`) with the working folder as the base of the default directory of the new project.
6. The system reports a summary that names the full path of the new project.
### Extensions (Alternative / Exception Flows)
- 1a. The `PATH` folder is not writable, or not on `PATH`:
1. The README names the other choices (a folder the Maintainer owns and adds to `PATH`, or an alias); the system is not involved.
- 3a. The command link is broken (the checkout was moved or removed):
1. The shell reports that the command cannot run; the README says how to recreate the link.
- 4a. The checkout's own files cannot be found from the link target:
1. The system stops before any change and names the folder it looked in.
- 4b. `config.env` or `.env` is not named, and is in neither the working folder nor the checkout:
1. The system stops before any change, names both places it looked in and says that `--config` and `--env` can point elsewhere.
- 4c. A chosen file comes from the working folder:
1. The system names the file and the Gitea address it holds and asks the Maintainer to confirm, default no, before any request to a host; on no, the system stops before any request and any change.
### Special Requirements / Business Rules
| Step | Rule |
| --- | --- |
| 1 | The command link is made by the Maintainer with the shell, not by the script; the script never edits `PATH`, a shell profile or a system folder |
| 4 | The system finds its own files by following the command link, however many links lie on the way, on every supported platform |
| 4 | `config.env` and `.env` are chosen one by one in this order: `--config` / `--env`; `./config.env` / `./.env` in the working folder; the checkout's. A project may therefore use its own `.env` with the checkout's `config.env` |
| 4 | A folder can hold a `config.env` that points the Gitea address elsewhere, and so send the token there. A file from the working folder is therefore confirmed before the first request, and every file used is named in the output |
| 5 | The base of the default directory is the working folder, never the checkout |
| 3 to 6 | Behaviour, prompts and summary are the same as when the script is started by its path from the checkout |
### Open Issues
- The README example is tested on Linux, macOS and Git Bash on Windows (MIL-007 criterion 6).
---
[UCD-001]: ../use-case-diagram.md
[US-001]: ../user-stories.md
[SA-001]: ../stakeholder-analysis.md
[BC-001]: ../business-case.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+10 -2
View File
@@ -4,12 +4,13 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | UCD-001 | | ID | UCD-001 |
| CrossReference | [SA-001], [BC-001], [US-001], [UC-001] | | CrossReference | [SA-001], [BC-001], [US-001], [UC-001], [UC-002] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] | | 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added UC-002 Start the script as a global command (includes UC-001) | [1cd27f7] |
--- ---
@@ -25,8 +26,11 @@ left to right direction
actor "Maintainer" as M <<Actor>> actor "Maintainer" as M <<Actor>>
rectangle "RepoFoundry" <<System>> { rectangle "RepoFoundry" <<System>> {
usecase "Create a new project" as UC1 usecase "Create a new project" as UC1
usecase "Start the script as a global command" as UC2
} }
M --> UC1 M --> UC1
M --> UC2
UC2 ..> UC1 : <<include>>
@enduml @enduml
``` ```
@@ -34,18 +38,20 @@ M --> UC1
| Actor | Stereotype | Stakeholder ID (SA) | Goals (use cases) | | Actor | Stereotype | Stakeholder ID (SA) | Goals (use cases) |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Maintainer | `<<Actor>>` | S01, S02 | Create a new project | | Maintainer | `<<Actor>>` | S01, S02 | Create a new project; start the script as a global command |
## Use Case Table ## Use Case Table
| Use Case | Actor(s) | Goal | | Use Case | Actor(s) | Goal |
| --- | --- | --- | | --- | --- | --- |
| Start the script as a global command ([UC-002]) | Maintainer | Start the script by name from the folder in which the new project is to be created |
| Create a new project ([UC-001]) | Maintainer | Start a new project with a Gitea repository, optionally a GitHub repository and mirror, and a local project with the SQA-QC-Framework | | Create a new project ([UC-001]) | Maintainer | Start a new project with a Gitea repository, optionally a GitHub repository and mirror, and a local project with the SQA-QC-Framework |
## Relationships ## Relationships
| From | Relationship (`<<include>>` / `<<extend>>`) | To | Justification | | From | Relationship (`<<include>>` / `<<extend>>`) | To | Justification |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| [UC-002] | `<<include>>` | [UC-001] | Starting by name always ends in creating a project; the project creation is the same whatever way the script was started |
| None | - | - | The optional GitHub steps are steps 5 and 7 of [UC-001], not a separate goal of the Maintainer, so they are not modelled as an `<<extend>>` use case | | None | - | - | The optional GitHub steps are steps 5 and 7 of [UC-001], not a separate goal of the Maintainer, so they are not modelled as an `<<extend>>` use case |
--- ---
@@ -54,4 +60,6 @@ M --> UC1
[BC-001]: ./business-case.md [BC-001]: ./business-case.md
[US-001]: ./user-stories.md [US-001]: ./user-stories.md
[UC-001]: ./uc-001/uc.md [UC-001]: ./uc-001/uc.md
[UC-002]: ./uc-002/uc.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
+80 -8
View File
@@ -4,13 +4,13 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | US-001 | | ID | US-001 |
| CrossReference | [BC-001], [UCD-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004] | | CrossReference | [BC-001], [UCD-001], [UC-002], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited UCD-001<br>Split the epic into three stories, one per milestone | [02875ae] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added US-002 (global command from the target folder) for UC-002 | [1cd27f7] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Added US-001.04: project details preset in config.env | [2a6bb8e] | | 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | US-002: the default configuration files are the working folder's, then the checkout's; confirmed and named | [0ab5006] |
--- ---
@@ -18,7 +18,7 @@
One epic: "Create a new project" ([UC-001]), setting up a new project on Gitea, optionally on GitHub, with the SQA-QC-Framework in place. The actor is the Maintainer, as in [UCD-001] (S01 or S02; for now one person holds both roles). One epic: "Create a new project" ([UC-001]), setting up a new project on Gitea, optionally on GitHub, with the SQA-QC-Framework in place. The actor is the Maintainer, as in [UCD-001] (S01 or S02; for now one person holds both roles).
The epic is split into four stories, one per milestone. Each story fits one two-week phase and can be shown working at the end of it. The epic is split into seven stories, one per milestone (US-001.01 to US-001.07). Each story fits one two-week phase and can be shown working at the end of it.
## Story List ## Story List
@@ -42,7 +42,7 @@ The epic is split into four stories, one per milestone. Each story fits one two-
**Acceptance Criteria** **Acceptance Criteria**
- Given valid tokens and owners, when the script runs, then a Gitea repository exists under the chosen owner: empty, or holding the AGPL license when GitHub was chosen. - Given valid tokens and owners, when the script runs, then a Gitea repository exists under the chosen owner: empty, or holding the license that applies (the one set in `PROJECT_LICENSE`, or AGPL-3.0 when GitHub was chosen and the project is public).
- Given GitHub was chosen, when the script runs, then an empty GitHub repository exists under its chosen owner (not assumed to be `GITHUB_USER`) and Gitea mirrors to it, and no credential is stored in any address. - Given GitHub was chosen, when the script runs, then an empty GitHub repository exists under its chosen owner (not assumed to be `GITHUB_USER`) and Gitea mirrors to it, and no credential is stored in any address.
- Given a step fails, when the script stops, then it reports what was created and how to continue. - Given a step fails, when the script stops, then it reports what was created and how to continue.
@@ -79,19 +79,91 @@ The epic is split into four stories, one per milestone. Each story fits one two-
| --- | --- | --- | | --- | --- | --- |
| [UC-001] step 3, [MIL-004] | fits one phase | Independent: needs the prompts of US-001.01 | | [UC-001] step 3, [MIL-004] | fits one phase | Independent: needs the prompts of US-001.01 |
### US-001.05 — Create a new project: ask for the credentials and keep them in the project
**As a** Maintainer, **I want** the script to ask for a credential that `.env` does not provide and to create a `.env` file in the new project, **so that** I can start without a prepared `.env` and the new project has the credentials its tools need.
**Acceptance Criteria**
- Given `GITEA_TOKEN` is not provided in `.env`, when the script starts, then it asks for it without showing what is typed and does not stop with an error; the same holds for `GITHUB_PAT` and `GITHUB_USER` when GitHub is chosen.
- Given an entered credential is not valid, when the script checks it, then it asks again and never shows the value.
- Given the project exists, when the Maintainer agrees, then the new project has a `.env` that holds only the credentials the project needs, is readable by its owner only and is ignored by git.
- Given the Maintainer declines, or `.env` already exists in the project and the Maintainer declines replacing it, then no `.env` is written or replaced and the summary says so.
- Given any run, then no credential appears in output, remotes, tracked files or the summary.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [UC-001] steps 2 and 9, [MIL-005] | fits one phase | Independent: needs the local project of US-001.03 |
### US-001.06 — Create a new project: choose the license in `config.env`
**As a** Maintainer, **I want** to set the project's license in `config.env`, **so that** a project is not forced to AGPL-3.0 by the GitHub choice, a private project is never given it by default, and no question is needed for it.
**Acceptance Criteria**
- Given `PROJECT_LICENSE` is set to a license the Gitea server offers, when the script creates the Gitea repository, then it holds that license, with or without GitHub, and the license is not asked.
- Given `PROJECT_LICENSE=none`, then the repository has no license even when GitHub is chosen.
- Given `PROJECT_LICENSE` is absent, then the license is AGPL-3.0 when GitHub is chosen and the project is public, and none otherwise (a private project with GitHub gets none).
- Given the value is empty or invalid, or the server does not offer it, when the script starts or checks the hosts, then it stops before anything is created and names the key or the license.
- Given GitHub is chosen, then the license reaches the GitHub repository through the mirror, as before.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [UC-001] steps 3, 4 and 6, [MIL-006] | fits one phase | Independent: needs the configurable details of US-001.04 |
### US-001.07 — Create a new project: the framework's own submodules are fetched
**As a** Maintainer, **I want** the new project to hold the framework together with its own submodules, **so that** the `qc` checklists are present without a manual step.
**Acceptance Criteria**
- Given the framework is added to the new project, when the step finishes, then `git submodule update --init --recursive` has run in the project and the framework's `qc` directory holds the checklists.
- Given `framework` already exists as the framework submodule, when the script runs again, then the same command runs, so an empty `qc` is filled and nothing else changes.
- Given the nested fetch fails, then the script stops that step, reports what exists, names the command to run by hand and does not show a credential.
- Given the framework has no submodule of its own, then the step changes nothing and does not fail.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [UC-001] step 9, [MIL-007] | fits one phase | Independent: needs the framework step of US-001.03 |
## Epic: Start the script as a global command
One further epic, "Start the script as a global command" ([UC-002]), with one story. The actor is the Maintainer.
### US-002 — Start the script by name from the folder where the project is to be created
**As a** Maintainer, **I want** to start the script by a name from any folder and have the project created in the folder I stand in, **so that** I do not have to enter the checkout or type its path each time.
**Acceptance Criteria**
- Given a command link in a folder on `PATH` that leads to the script, when the Maintainer starts it by name from another folder, then the script runs and finds its own files.
- Given the Maintainer stands in a folder, when the project directory is not preset, then its default is `./<name>` under that folder, never under the checkout.
- Given `--config` and `--env` name files, then those are read. Given they are not named, then `./config.env` and `./.env` in the folder the Maintainer stands in are read, each one that exists, and the checkout's file stands in for one that does not.
- Given a file comes from the folder the Maintainer stands in, then the script names it and the Gitea address it holds and asks for a yes, default no, before any request; every file used is named in the output.
- Given a file is named nowhere, in neither folder, or a link is broken, then the script stops before any change and names both places it looked in.
- Given the README, then it shows the command that makes the link, the check that it works and a run from a folder that is not the checkout.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [UC-002], [MIL-007] | fits one phase | Independent: needs the script of [UC-001] |
## INVEST Check ## INVEST Check
Valuable, Negotiable, Estimable, Small and Testable hold for each story. Independent holds only in part: the stories are ordered, each using what the one before it delivers, which follows the milestone order in [PP-001]. This is flagged as an exception on US-001.02, US-001.03 and US-001.04. Valuable, Negotiable, Estimable, Small and Testable hold for each story. Independent holds only in part: the stories are ordered, each using what the one before it delivers, which follows the milestone order in [PP-001]. This is flagged as an exception on US-001.02 to US-001.07 and on US-002.
--- ---
[BC-001]: ./business-case.md [BC-001]: ./business-case.md
[UCD-001]: ./use-case-diagram.md [UCD-001]: ./use-case-diagram.md
[UC-001]: ./uc-001/uc.md [UC-001]: ./uc-001/uc.md
[UC-002]: ./uc-002/uc.md
[MIL-001]: ./milestones/mil-001-foundation.md [MIL-001]: ./milestones/mil-001-foundation.md
[MIL-002]: ./milestones/mil-002-repositories-and-mirror.md [MIL-002]: ./milestones/mil-002-repositories-and-mirror.md
[MIL-003]: ./milestones/mil-003-scaffold-and-release.md [MIL-003]: ./milestones/mil-003-scaffold-and-release.md
[MIL-004]: ./milestones/mil-004-configurable-details.md [MIL-004]: ./milestones/mil-004-configurable-details.md
[MIL-005]: ./milestones/mil-005-credentials.md
[MIL-006]: ./milestones/mil-006-project-license.md
[MIL-007]: ./milestones/mil-007-framework-checklists.md
[PP-001]: ./project-plan.md [PP-001]: ./project-plan.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62 [0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+48 -16
View File
@@ -10,8 +10,11 @@
# repositories and the mirror, then the local project: its directory, git # repositories and the mirror, then the local project: its directory, git
# repository, remotes (no credential in any address), the framework as a # repository, remotes (no credential in any address), the framework as a
# submodule, the framework's skills and git hooks (and the plan gate if # submodule, the framework's skills and git hooks (and the plan gate if
# chosen) and its templates. Choosing GitHub also applies the AGPL-3.0 # chosen) and its templates. The license of the Gitea repository is
# license to the Gitea repository. No commit is made in the new project. # PROJECT_LICENSE in config.env (none means no license); without it AGPL-3.0
# applies only when GitHub is chosen and the project is public. After a yes
# (default no) it also writes the new project's own .env with the credentials
# the project needs. No commit is made in the new project.
# #
# Dry run by default # Dry run by default
# Without --apply the script only reads from GitHub and Gitea (GET # Without --apply the script only reads from GitHub and Gitea (GET
@@ -27,7 +30,8 @@
# Options # Options
# --apply create the repositories and the mirror (after a final yes) # --apply create the repositories and the mirror (after a final yes)
# --config FILE service addresses (default: config.env in the project root) # --config FILE service addresses (default: config.env in the project root)
# --env FILE credentials (default: .env in the project root) # --env FILE credentials (default: .env in the project root); optional:
# a credential it does not provide is asked, not echoed
# -h, --help show this help # -h, --help show this help
# --version show the version # --version show the version
# #
@@ -36,7 +40,8 @@
# the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL # the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL
# (default 10m0s) and FRAMEWORK_REPO (default # (default 10m0s) and FRAMEWORK_REPO (default
# TirSystem/SQA-QC-Framework, the submodule's OWNER/NAME) # TirSystem/SQA-QC-Framework, the submodule's OWNER/NAME)
# .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN # .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN (all optional, each
# asked when missing)
# #
# Environment # Environment
# REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry) # REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry)
@@ -65,8 +70,8 @@
# This file is the entry point. The work is split by responsibility into # This file is the entry point. The work is split by responsibility into
# the files in lib/ next to it (one job per file, see the first lines of # the files in lib/ next to it (one job per file, see the first lines of
# each file): constants, output, temp, util, validate, config, tools, json, # each file): constants, output, temp, util, validate, config, tools, json,
# http, api, prompts, project, hosts, preflight, steps, plan, repositories, # http, api, prompts, credentials, project, hosts, preflight, steps, plan,
# mirror, git, localproject, framework, apply and cli. The files are loaded # repositories, mirror, git, localproject, framework, envfile, apply and cli. The files are loaded
# from this directory only. # from this directory only.
# #
# Exit codes # Exit codes
@@ -85,18 +90,39 @@ if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4)));
fi fi
# Where this script lives; the library files and the project root are found # Where this script lives; the library files and the project root are found
# from here, never from the current directory. # from here, never from the current directory. A link to the script (a
readonly SCRIPT_FILE="${BASH_SOURCE[0]}" # command in a folder on PATH) is followed to the real file, however many
case "${BASH_SOURCE[0]}" in # links lie on the way; readlink without -f exists on Linux, macOS and Git
*/*) script_path_dir="${BASH_SOURCE[0]%/*}" ;; # Bash alike.
script_path="${BASH_SOURCE[0]}"
script_link_count=0
while [[ -L $script_path ]]; do
((++script_link_count <= 40)) || {
printf 'error: too many links when following %s\n' "${BASH_SOURCE[0]}" >&2
exit 1
}
script_link_target="$(readlink -- "$script_path")"
case "$script_link_target" in
/*) script_path="$script_link_target" ;;
*)
case "$script_path" in
*/*) script_path="${script_path%/*}/$script_link_target" ;;
*) script_path="$script_link_target" ;;
esac
;;
esac
done
readonly SCRIPT_FILE="$script_path"
case "$script_path" in
*/*) script_path_dir="${script_path%/*}" ;;
*) script_path_dir="." ;; *) script_path_dir="." ;;
esac esac
SCRIPT_DIR="$(cd "$script_path_dir" && pwd)" SCRIPT_DIR="$(cd -P "$script_path_dir" && pwd -P)"
readonly SCRIPT_DIR readonly SCRIPT_DIR
unset script_path_dir unset script_path script_path_dir script_link_count script_link_target
# The script lives in src/; the configuration files live one level up, in # The script lives in src/; the checkout is one level up, next to
# the project root, next to config.env.example and .env.example. # config.env.example and .env.example.
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd -P "$SCRIPT_DIR/.." && pwd -P)"
readonly PROJECT_ROOT readonly PROJECT_ROOT
# shellcheck source=lib/constants.sh # shellcheck source=lib/constants.sh
@@ -121,6 +147,8 @@ source "$SCRIPT_DIR/lib/http.sh"
source "$SCRIPT_DIR/lib/api.sh" source "$SCRIPT_DIR/lib/api.sh"
# shellcheck source=lib/prompts.sh # shellcheck source=lib/prompts.sh
source "$SCRIPT_DIR/lib/prompts.sh" source "$SCRIPT_DIR/lib/prompts.sh"
# shellcheck source=lib/credentials.sh
source "$SCRIPT_DIR/lib/credentials.sh"
# shellcheck source=lib/project.sh # shellcheck source=lib/project.sh
source "$SCRIPT_DIR/lib/project.sh" source "$SCRIPT_DIR/lib/project.sh"
# shellcheck source=lib/hosts.sh # shellcheck source=lib/hosts.sh
@@ -141,6 +169,8 @@ source "$SCRIPT_DIR/lib/git.sh"
source "$SCRIPT_DIR/lib/localproject.sh" source "$SCRIPT_DIR/lib/localproject.sh"
# shellcheck source=lib/framework.sh # shellcheck source=lib/framework.sh
source "$SCRIPT_DIR/lib/framework.sh" source "$SCRIPT_DIR/lib/framework.sh"
# shellcheck source=lib/envfile.sh
source "$SCRIPT_DIR/lib/envfile.sh"
# shellcheck source=lib/apply.sh # shellcheck source=lib/apply.sh
source "$SCRIPT_DIR/lib/apply.sh" source "$SCRIPT_DIR/lib/apply.sh"
# shellcheck source=lib/cli.sh # shellcheck source=lib/cli.sh
@@ -162,13 +192,15 @@ main() {
trap finish EXIT trap finish EXIT
is_valid_repo_name "$PROJECT_NAME" || is_valid_repo_name "$PROJECT_NAME" ||
die "REPOFOUNDRY_NAME is not a valid project name" die "REPOFOUNDRY_NAME is not a valid project name"
WORKING_FOLDER="$(pwd -P)"
parse_args "$@" parse_args "$@"
check_tools check_tools
setup_temp_dir setup_temp_dir
load_configuration load_configuration
collect_credentials GITEA_TOKEN
collect_project_details collect_project_details
if ((PROJECT[has_github])); then if ((PROJECT[has_github])); then
require_github_credentials collect_credentials GITHUB_PAT GITHUB_USER
fi fi
init_steps init_steps
print_summary print_summary
+3 -2
View File
@@ -19,6 +19,7 @@ create_all() {
add_framework add_framework
install_framework install_framework
copy_templates copy_templates
create_env_file
} }
# confirm_framework_access: the framework comes over SSH. Without SSH the # confirm_framework_access: the framework comes over SSH. Without SSH the
@@ -47,9 +48,9 @@ confirm_reuse() {
die "stopped: choose another name or remove the existing repository" die "stopped: choose another name or remove the existing repository"
fi fi
done done
if ((${STATE[reuse_gitea]:-0})) && ((PROJECT[has_github])) && if ((${STATE[reuse_gitea]:-0})) && [[ -n ${PROJECT[license]} ]] &&
[[ ${STATE[gitea_repo]} == empty ]]; then [[ ${STATE[gitea_repo]} == empty ]]; then
warn "the empty Gitea repository is reused as it is: the $AGPL_LICENSE_KEY license is not added to it" warn "the empty Gitea repository is reused as it is: the ${PROJECT[license]} license is not added to it"
fi fi
} }
+2
View File
@@ -9,6 +9,8 @@
usage() { usage() {
cat <<EOF cat <<EOF
Usage: ${0##*/} [--apply] [--config FILE] [--env FILE] Usage: ${0##*/} [--apply] [--config FILE] [--env FILE]
Without --config and --env, ./config.env and ./.env in the current folder
are read, then the ones in the checkout.
${0##*/} --help | --version ${0##*/} --help | --version
EOF EOF
} }
+69 -20
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: unquote_value, parse_env_file, parse_env_entry, validate_config, check_preset, check_preset_choice, validate_project_presets, validate_credentials, require_github_credentials, warn_if_env_unsafe, load_configuration # Provides: unquote_value, parse_env_file, parse_env_entry, validate_config, check_preset, check_preset_choice, validate_project_presets, validate_credentials, warn_if_env_unsafe, load_configuration
# unquote_value RAW: strip matching quotes (or a trailing " # comment" on an # unquote_value RAW: strip matching quotes (or a trailing " # comment" on an
# unquoted value) and return the value in REPLY. Fails on unbalanced quotes. # unquoted value) and return the value in REPLY. Fails on unbalanced quotes.
@@ -142,22 +142,26 @@ validate_project_presets() {
check_preset GITHUB_OWNER is_valid_github_owner "$HINT_GITHUB_OWNER" check_preset GITHUB_OWNER is_valid_github_owner "$HINT_GITHUB_OWNER"
check_preset PROJECT_DIRECTORY is_valid_directory "$HINT_DIRECTORY" check_preset PROJECT_DIRECTORY is_valid_directory "$HINT_DIRECTORY"
check_preset_choice ENABLE_PLAN_GATE yes no check_preset_choice ENABLE_PLAN_GATE yes no
check_preset PROJECT_LICENSE is_valid_license "$HINT_LICENSE"
} }
# validate_credentials: check the credentials that .env provides. A credential
# that is not provided is not an error: it is asked later (collect_credentials).
validate_credentials() { validate_credentials() {
if [[ -z ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then
die "GITEA_TOKEN is missing in $ENV_FILE (see .env.example)"
fi
# Register secrets first so that no later message can show them. # Register secrets first so that no later message can show them.
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}") if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}")
fi
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]]; then if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]]; then
SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}") SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}")
fi fi
is_valid_token "${CREDENTIALS[GITEA_TOKEN]}" || if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]] &&
die "GITEA_TOKEN in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)" ! is_valid_token "${CREDENTIALS[GITEA_TOKEN]}"; then
die "GITEA_TOKEN in $ENV_FILE is not a valid token ($HINT_TOKEN)"
fi
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]] && if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]] &&
! is_valid_token "${CREDENTIALS[GITHUB_PAT]}"; then ! is_valid_token "${CREDENTIALS[GITHUB_PAT]}"; then
die "GITHUB_PAT in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)" die "GITHUB_PAT in $ENV_FILE is not a valid token ($HINT_TOKEN)"
fi fi
if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] && if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] &&
! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then ! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then
@@ -165,16 +169,6 @@ validate_credentials() {
fi fi
} }
# GitHub credentials are only needed when the Maintainer chose GitHub.
require_github_credentials() {
local key
for key in GITHUB_PAT GITHUB_USER; do
if [[ -z ${CREDENTIALS[$key]:-} ]]; then
die "GitHub was chosen but $key is missing in $ENV_FILE (see .env.example)"
fi
done
}
warn_if_env_unsafe() { warn_if_env_unsafe() {
local file="$1" dir mode local file="$1" dir mode
case "$(uname -s 2>/dev/null || true)" in case "$(uname -s 2>/dev/null || true)" in
@@ -197,10 +191,65 @@ warn_if_env_unsafe() {
fi fi
} }
# locate_config_file NAME FLAG FILE_VAR ORIGIN_VAR: the file named with FLAG;
# otherwise ./NAME in the working folder; otherwise NAME in the checkout.
# The origin is "named", "folder" or "checkout". With a fifth word, optional,
# a file found nowhere is not an error: the file stays empty and the origin
# is "none".
locate_config_file() {
local name="$1" flag="$2" optional="${5:-}"
local -n file_ref="$3" origin_ref="$4"
if [[ -n $file_ref ]]; then
origin_ref="named"
elif [[ $WORKING_FOLDER != "$PROJECT_ROOT" && -f $WORKING_FOLDER/$name ]]; then
file_ref="$WORKING_FOLDER/$name"
origin_ref="folder"
elif [[ -f $PROJECT_ROOT/$name ]]; then
file_ref="$PROJECT_ROOT/$name"
origin_ref="checkout"
elif [[ -n $optional ]]; then
origin_ref="none"
else
die "$name is not named with $flag and is in neither the working folder ($WORKING_FOLDER) nor the checkout ($PROJECT_ROOT); name it with $flag FILE"
fi
}
# origin_words ORIGIN: how the origin of a configuration file reads.
origin_words() {
case "$1" in
named) printf 'named on the command line' ;;
none) printf 'none found; a missing credential is asked' ;;
folder) printf 'from the working folder' ;;
*) printf 'from the checkout' ;;
esac
}
# confirm_folder_files: a file taken from the working folder may point the
# Gitea address elsewhere and so send the token there, so it is named with
# the address and needs a yes (default no) before the first request.
confirm_folder_files() {
local files=()
[[ $CONFIG_ORIGIN == folder ]] && files+=("$CONFIG_FILE")
[[ $ENV_ORIGIN == folder ]] && files+=("$ENV_FILE")
((${#files[@]} > 0)) || return 0
say "The working folder supplies: ${files[*]}"
say "Gitea would be ${CONFIG[GITEA_URL]}; the token from the credentials file is sent there."
prompt_yes_no "Use ${files[*]}" n
((REPLY)) || die "stopped before any request: choose the files with --config and --env, or run from the checkout"
}
load_configuration() { load_configuration() {
locate_config_file config.env --config CONFIG_FILE CONFIG_ORIGIN
locate_config_file .env --env ENV_FILE ENV_ORIGIN optional
say "Config file : $CONFIG_FILE ($(origin_words "$CONFIG_ORIGIN"))"
say "Credentials file: ${ENV_FILE:-(none)} ($(origin_words "$ENV_ORIGIN"))"
parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG
validate_config validate_config
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS confirm_folder_files
# .env is optional: a credential it does not provide is asked.
if [[ -n $ENV_FILE && -e $ENV_FILE ]]; then
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
warn_if_env_unsafe "$ENV_FILE"
fi
validate_credentials validate_credentials
warn_if_env_unsafe "$ENV_FILE"
} }
+14 -4
View File
@@ -18,6 +18,7 @@ readonly DEFAULT_MIRROR_INTERVAL="10m0s"
readonly DEFAULT_SSH_PORT=10022 readonly DEFAULT_SSH_PORT=10022
readonly DEFAULT_FRAMEWORK_REPO="TirSystem/SQA-QC-Framework" readonly DEFAULT_FRAMEWORK_REPO="TirSystem/SQA-QC-Framework"
readonly AGPL_LICENSE_KEY="AGPL-3.0" readonly AGPL_LICENSE_KEY="AGPL-3.0"
readonly NO_LICENSE_WORD="none"
readonly DEFAULT_BRANCH="main" readonly DEFAULT_BRANCH="main"
# What the prompts and the preset keys in config.env both tell the Maintainer # What the prompts and the preset keys in config.env both tell the Maintainer
# when a value is refused. # when a value is refused.
@@ -25,18 +26,27 @@ readonly HINT_REPO_NAME="use letters, digits, '.', '_' or '-' (at most 100), not
readonly HINT_DESCRIPTION="at most $MAX_DESCRIPTION_LENGTH characters and no control characters" readonly HINT_DESCRIPTION="at most $MAX_DESCRIPTION_LENGTH characters and no control characters"
readonly HINT_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)" readonly HINT_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)"
readonly HINT_GITHUB_OWNER="use letters, digits or '-' (at most 39)" readonly HINT_GITHUB_OWNER="use letters, digits or '-' (at most 39)"
readonly HINT_LICENSE="use a Gitea license key (letters, digits, '.', '+' or '-', at most 64), such as AGPL-3.0 or MIT, or none"
readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters" readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters"
readonly HINT_TOKEN="8 to 255 letters, digits or _ . ~ + / = -"
readonly ENV_FILE_NAME=".env"
readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror" readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
"Local project" "Framework" "Skills and hooks" "Templates") "Local project" "Framework" "Skills and hooks" "Templates" "Project .env")
# shellcheck disable=SC2034 # read through namerefs (parse_env_file) # shellcheck disable=SC2034 # read through namerefs (parse_env_file)
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
PROJECT_NAME PROJECT_DESCRIPTION PROJECT_VISIBILITY GITEA_OWNER USE_GITHUB PROJECT_NAME PROJECT_DESCRIPTION PROJECT_VISIBILITY GITEA_OWNER USE_GITHUB
GITHUB_OWNER PROJECT_DIRECTORY ENABLE_PLAN_GATE) GITHUB_OWNER PROJECT_DIRECTORY ENABLE_PLAN_GATE PROJECT_LICENSE)
readonly CREDENTIAL_KEYS=(GITHUB_PAT GITHUB_USER GITEA_TOKEN) readonly CREDENTIAL_KEYS=(GITHUB_PAT GITHUB_USER GITEA_TOKEN)
CONFIG_FILE="$PROJECT_ROOT/config.env" # The configuration files: named by --config and --env, or chosen by
ENV_FILE="$PROJECT_ROOT/.env" # locate_config_file. The origin is named, folder or checkout.
CONFIG_FILE=""
ENV_FILE=""
CONFIG_ORIGIN=""
ENV_ORIGIN=""
# The folder the Maintainer started the script in.
WORKING_FOLDER=""
TMP_DIR="" TMP_DIR=""
HAS_JQ=0 HAS_JQ=0
HTTP_STATUS=0 HTTP_STATUS=0
+42
View File
@@ -0,0 +1,42 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# credentials.sh - Asking for a credential that .env does not provide.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: credential_label, collect_credentials
# credential_label KEY: the name of a credential as the Maintainer sees it.
credential_label() {
case "$1" in
GITEA_TOKEN) printf 'Gitea access token' ;;
GITHUB_PAT) printf 'GitHub personal access token' ;;
GITHUB_USER) printf 'GitHub account name (the account the token belongs to)' ;;
*) printf '%s' "$1" ;;
esac
}
# collect_credentials KEY...: ask for each credential that is not already
# provided. A token is read without echo and registered as a secret at once,
# so no later message can show it; the GitHub account name is not secret and
# is read like any other answer. An empty value in .env counts as not provided.
collect_credentials() {
local key
for key in "$@"; do
if [[ -n ${CREDENTIALS[$key]:-} ]]; then
continue
fi
case "$key" in
GITHUB_USER)
prompt_value "$(credential_label "$key")" "" is_valid_github_owner \
"use letters, digits or '-' (at most 39)"
;;
*)
prompt_secret "$(credential_label "$key")" is_valid_token "$HINT_TOKEN"
SECRET_VALUES+=("$REPLY")
;;
esac
CREDENTIALS[$key]="$REPLY"
REPLY=""
done
}
+97
View File
@@ -0,0 +1,97 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# envfile.sh - The .env file of the new project: the one place a credential is written.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: env_file_keys, env_file_key_list, exclude_env_file, write_env_file, create_env_file
# env_file_keys: the credentials the new project needs, one per line: the
# Gitea token, and the GitHub token and account name when GitHub was chosen.
env_file_keys() {
printf '%s\n' GITEA_TOKEN
if ((PROJECT[has_github])); then
printf '%s\n' GITHUB_PAT GITHUB_USER
fi
}
# env_file_key_list: the same keys on one line, for messages.
env_file_key_list() {
local keys
keys="$(env_file_keys | tr '\n' ' ')"
printf '%s' "${keys% }"
}
# exclude_env_file DIR: make git ignore .env in DIR without touching a tracked
# file: the entry goes into .git/info/exclude, which is never committed. It
# does nothing when .env is already ignored.
exclude_env_file() {
local dir="$1" gitdir exclude
if git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME"; then
return 0
fi
gitdir="$(git_project "$dir" rev-parse --absolute-git-dir)"
exclude="$gitdir/info/exclude"
mkdir -p -- "$gitdir/info"
# Start on a fresh line when the file does not end with one.
if [[ -s $exclude && -n "$(tail -c 1 -- "$exclude")" ]]; then
printf '\n' >>"$exclude"
fi
printf '%s\n' "# RepoFoundry: the credentials file of this project" "$ENV_FILE_NAME" >>"$exclude"
git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME" ||
die "could not make git ignore $ENV_FILE_NAME in $dir; nothing was written to it"
}
# write_env_file DIR IS_REPLACE: write the credentials to DIR/.env. The file is
# created private (mode 600) from the start, never readable by others, even
# for a moment: it is written under umask 077 as a temporary file next to the
# target and moved into place. An existing file is only replaced when
# IS_REPLACE is 1, and a file that appears in the meantime is never replaced.
write_env_file() {
local dir="$1" is_replace="$2" target tmp key
target="$dir/$ENV_FILE_NAME"
tmp="$(umask 077 && mktemp "$dir/$ENV_FILE_NAME.XXXXXX")"
TEMP_FILES+=("$tmp")
{
while IFS= read -r key; do
printf '%s=%s\n' "$key" "${CREDENTIALS[$key]}"
done < <(env_file_keys)
} >"$tmp"
if ((is_replace)); then
mv -f -- "$tmp" "$target"
else
mv -n -- "$tmp" "$target"
if [[ -e $tmp ]]; then
die "$target appeared while it was being written; it was not replaced"
fi
fi
}
# create_env_file: the last step. Only after a yes (default no) is the .env
# written, and an existing one is only replaced after another yes. Nothing
# printed names a value, only the keys.
create_env_file() {
local label="Project .env" dir="${PROJECT[directory]}" keys is_replace=0
keys="$(env_file_key_list)"
begin_step "$label"
prompt_yes_no "Create a $ENV_FILE_NAME file in the project with the credentials it needs ($keys); only you can read it and git ignores it" n
if ! ((REPLY)); then
finish_step "$label" "skipped" "(you declined)"
return 0
fi
if git_project "$dir" ls-files --error-unmatch -- "$ENV_FILE_NAME" >/dev/null 2>&1; then
finish_step "$label" "skipped" "($ENV_FILE_NAME is tracked by git; it was not written)"
return 0
fi
if [[ -e $dir/$ENV_FILE_NAME || -L $dir/$ENV_FILE_NAME ]]; then
prompt_yes_no "$ENV_FILE_NAME already exists in the project. Replace it" n
if ! ((REPLY)); then
finish_step "$label" "kept" "(the existing $ENV_FILE_NAME was left as it was)"
return 0
fi
is_replace=1
fi
exclude_env_file "$dir"
write_env_file "$dir" "$is_replace"
finish_step "$label" "created" "($keys; only you can read it, git ignores it)"
}
+15 -1
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: is_framework_skipped, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates # Provides: is_framework_skipped, init_framework_submodules, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates
# is_framework_skipped: succeed when the framework steps are left out because # is_framework_skipped: succeed when the framework steps are left out because
# SSH to Gitea is not available (the Maintainer agreed to that). # SSH to Gitea is not available (the Maintainer agreed to that).
@@ -12,6 +12,18 @@ is_framework_skipped() {
[[ ${STATE[skip_framework]:-0} == 1 ]] [[ ${STATE[skip_framework]:-0} == 1 ]]
} }
# init_framework_submodules DIR: fetch the submodules the framework holds
# itself (the qc checklists). Without a submodule of its own this changes
# nothing. A failure names the command to run by hand.
init_framework_submodules() {
local dir="$1" err
make_temp_file
err="$REPLY"
if ! git_project "$dir" submodule update -q --init --recursive 2>"$err"; then
die "the framework was added but git could not fetch its own submodules (the qc checklists). Run in $dir: git submodule update --init --recursive. Git said: $(head -n 2 "$err" | tr '\n' ' ')"
fi
}
# add_framework: git submodule add of the framework as "framework". SSH is # add_framework: git submodule add of the framework as "framework". SSH is
# needed for it; a failure says how to test the access. # needed for it; a failure says how to test the access.
add_framework() { add_framework() {
@@ -27,6 +39,7 @@ add_framework() {
if [[ $existing != "$url" ]]; then if [[ $existing != "$url" ]]; then
die "'framework' already exists in $dir and is not the framework submodule ($url)" die "'framework' already exists in $dir and is not the framework submodule ($url)"
fi fi
init_framework_submodules "$dir"
finish_step "$label" "reused" "$url (already a submodule)" finish_step "$label" "reused" "$url (already a submodule)"
return 0 return 0
fi fi
@@ -35,6 +48,7 @@ add_framework() {
if ! git_project "$dir" submodule add -q "$url" framework 2>"$err"; then if ! git_project "$dir" submodule add -q "$url" framework 2>"$err"; then
die "git could not add the framework from $url. Check the SSH access first: ssh -p ${CONFIG[GITEA_SSH_PORT]} -T git@$(gitea_host). Git said: $(head -n 2 "$err" | tr '\n' ' ')" die "git could not add the framework from $url. Check the SSH access first: ssh -p ${CONFIG[GITEA_SSH_PORT]} -T git@$(gitea_host). Git said: $(head -n 2 "$err" | tr '\n' ' ')"
fi fi
init_framework_submodules "$dir"
finish_step "$label" "created" "$url" finish_step "$label" "created" "$url"
} }
+3 -1
View File
@@ -72,7 +72,7 @@ checkout_gitea_history() {
} }
# create_local_project: the directory, the git repository on main, the # create_local_project: the directory, the git repository on main, the
# remotes and, when GitHub is chosen, the license history. No commit is made. # remotes and, when a license applies, the license history. No commit is made.
create_local_project() { create_local_project() {
local label="Local project" dir="${PROJECT[directory]}" local label="Local project" dir="${PROJECT[directory]}"
begin_step "$label" begin_step "$label"
@@ -84,6 +84,8 @@ create_local_project() {
ensure_remote "$dir" origin "$(origin_url)" ensure_remote "$dir" origin "$(origin_url)"
if ((PROJECT[has_github])); then if ((PROJECT[has_github])); then
ensure_remote "$dir" github "$(github_remote_url)" ensure_remote "$dir" github "$(github_remote_url)"
fi
if [[ -n ${PROJECT[license]} ]]; then
checkout_gitea_history "$dir" checkout_gitea_history "$dir"
fi fi
finish_step "$label" "created" "$dir (origin over $(origin_protocol))" finish_step "$label" "created" "$dir (origin over $(origin_protocol))"
+3 -2
View File
@@ -22,8 +22,8 @@ print_plan() {
say "Plan:" say "Plan:"
if ((STATE[reuse_gitea])); then if ((STATE[reuse_gitea])); then
gitea_action="reuse the existing repository (you will be asked to confirm)" gitea_action="reuse the existing repository (you will be asked to confirm)"
elif ((PROJECT[has_github])); then elif [[ -n ${PROJECT[license]} ]]; then
gitea_action="create (${PROJECT[visibility]}) with the $AGPL_LICENSE_KEY license" gitea_action="create (${PROJECT[visibility]}) with the ${PROJECT[license]} license$(license_note)"
else else
gitea_action="create (${PROJECT[visibility]}), empty" gitea_action="create (${PROJECT[visibility]}), empty"
fi fi
@@ -54,4 +54,5 @@ print_plan() {
else else
say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")" say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")"
fi fi
say "$(printf ' %-18s: %s' "Project .env" "you are asked whether to create it ($(env_file_key_list))")"
} }
+4 -4
View File
@@ -23,8 +23,8 @@ check_gitea_organization() {
check_gitea_license() { check_gitea_license() {
api_call gitea GET /licenses api_call gitea GET /licenses
expect_status "cannot list the licenses of the Gitea server" 200 expect_status "cannot list the licenses of the Gitea server" 200
json_has_value "$HTTP_BODY_FILE" key "$AGPL_LICENSE_KEY" || json_has_value "$HTTP_BODY_FILE" key "${PROJECT[license]}" ||
die "the Gitea server does not offer the $AGPL_LICENSE_KEY license" die "the Gitea server does not offer the ${PROJECT[license]} license"
} }
# inspect_repository HOST: record in STATE[HOST_repo] whether the repository # inspect_repository HOST: record in STATE[HOST_repo] whether the repository
@@ -70,7 +70,7 @@ preflight_gitea() {
check_gitea_organization "$owner" "$login" check_gitea_organization "$owner" "$login"
STATE[gitea_owner_kind]="organization" STATE[gitea_owner_kind]="organization"
fi fi
if ((PROJECT[has_github])); then if [[ -n ${PROJECT[license]} ]]; then
check_gitea_license check_gitea_license
fi fi
inspect_repository gitea inspect_repository gitea
@@ -143,7 +143,7 @@ decide_existing_repositories() {
free) ;; free) ;;
empty) STATE[reuse_$host]=1 ;; empty) STATE[reuse_$host]=1 ;;
initial_only) initial_only)
if [[ $host == gitea ]] && ((PROJECT[has_github])); then if [[ $host == gitea && -n ${PROJECT[license]} ]]; then
STATE[reuse_$host]=1 STATE[reuse_$host]=1
else else
die "the $(host_label "$host") repository $owner/${PROJECT[name]} already exists and has content; choose another name or remove it first" die "the $(host_label "$host") repository $owner/${PROJECT[name]} already exists and has content; choose another name or remove it first"
+32 -3
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: preset_detail, collect_project_details, collect_github_details, source_note, yes_no, credential_state, print_summary # Provides: preset_detail, resolve_license, license_note, collect_project_details, collect_github_details, source_note, yes_no, credential_state, print_summary
# preset_detail KEY NAME: a detail set in config.env is used and not asked; # preset_detail KEY NAME: a detail set in config.env is used and not asked;
# the value is returned in REPLY and marked in PRESET[NAME]. # the value is returned in REPLY and marked in PRESET[NAME].
@@ -14,6 +14,33 @@ preset_detail() {
PRESET[$2]=1 PRESET[$2]=1
} }
# resolve_license: the license that applies, in PROJECT[license] (empty means
# none). PROJECT_LICENSE in config.env decides, and "none" means no license;
# without it AGPL-3.0 applies only when GitHub is chosen and the project is
# public. The license is never asked.
resolve_license() {
PROJECT[license]=""
if [[ -n ${CONFIG[PROJECT_LICENSE]+set} ]]; then
PRESET[license]=1
if [[ ${CONFIG[PROJECT_LICENSE],,} != "$NO_LICENSE_WORD" ]]; then
PROJECT[license]="${CONFIG[PROJECT_LICENSE]}"
fi
elif ((PROJECT[has_github])) && [[ ${PROJECT[visibility]} == public ]]; then
PROJECT[license]="$AGPL_LICENSE_KEY"
fi
}
# license_note: where the license on the Gitea repository comes from.
license_note() {
if [[ -n ${PRESET[license]:-} ]]; then
source_note license
elif [[ -n ${PROJECT[license]} ]]; then
printf ' (default: GitHub and a public project)'
elif ((PROJECT[has_github])); then
printf ' (no default for a private project)'
fi
}
# Ask for each project detail, except those set in config.env. # Ask for each project detail, except those set in config.env.
collect_project_details() { collect_project_details() {
preset_detail PROJECT_NAME name || preset_detail PROJECT_NAME name ||
@@ -29,6 +56,7 @@ collect_project_details() {
prompt_value "Gitea owner (user or organization)" "" is_valid_gitea_owner "$HINT_GITEA_OWNER" prompt_value "Gitea owner (user or organization)" "" is_valid_gitea_owner "$HINT_GITEA_OWNER"
PROJECT[gitea_owner]="$REPLY" PROJECT[gitea_owner]="$REPLY"
collect_github_details collect_github_details
resolve_license
preset_detail PROJECT_DIRECTORY directory || preset_detail PROJECT_DIRECTORY directory ||
prompt_value "Local directory" "./${PROJECT[name]}" is_valid_directory "$HINT_DIRECTORY" prompt_value "Local directory" "./${PROJECT[name]}" is_valid_directory "$HINT_DIRECTORY"
PROJECT[directory]="$REPLY" PROJECT[directory]="$REPLY"
@@ -46,7 +74,7 @@ collect_github_details() {
if preset_detail USE_GITHUB has_github; then if preset_detail USE_GITHUB has_github; then
[[ $REPLY == yes ]] && REPLY=1 || REPLY=0 [[ $REPLY == yes ]] && REPLY=1 || REPLY=0
else else
prompt_yes_no "Also create a GitHub repository (applies the AGPL license)" y prompt_yes_no "Also create a GitHub repository" y
fi fi
PROJECT[has_github]="$REPLY" PROJECT[has_github]="$REPLY"
PROJECT[github_owner]="" PROJECT[github_owner]=""
@@ -90,10 +118,11 @@ print_summary() {
say " Description : ${PROJECT[description]:-(none)}$(source_note description)" say " Description : ${PROJECT[description]:-(none)}$(source_note description)"
say " Gitea : ${CONFIG[GITEA_URL]}/${PROJECT[gitea_owner]}/${PROJECT[name]}$(source_note gitea_owner)" say " Gitea : ${CONFIG[GITEA_URL]}/${PROJECT[gitea_owner]}/${PROJECT[name]}$(source_note gitea_owner)"
if ((PROJECT[has_github])); then if ((PROJECT[has_github])); then
say " GitHub : ${CONFIG[GITHUB_WEB_URL]}/${PROJECT[github_owner]}/${PROJECT[name]} (AGPL license applied)$(source_note github_owner)" say " GitHub : ${CONFIG[GITHUB_WEB_URL]}/${PROJECT[github_owner]}/${PROJECT[name]}$(source_note github_owner)"
else else
say " GitHub : not used$(source_note has_github)" say " GitHub : not used$(source_note has_github)"
fi fi
say " License : ${PROJECT[license]:-none}$(license_note)"
say " Directory : ${PROJECT[directory]}$(source_note directory)" say " Directory : ${PROJECT[directory]}$(source_note directory)"
say " Plan gate : $(yes_no "${PROJECT[is_plan_gate_enabled]}")$(source_note is_plan_gate_enabled)" say " Plan gate : $(yes_no "${PROJECT[is_plan_gate_enabled]}")$(source_note is_plan_gate_enabled)"
say "Credentials : GITEA_TOKEN $(credential_state GITEA_TOKEN)," \ say "Credentials : GITEA_TOKEN $(credential_state GITEA_TOKEN)," \
+22 -1
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: prompt_value, prompt_choice, prompt_yes_no # Provides: prompt_value, prompt_secret, prompt_choice, prompt_yes_no
# prompt_value LABEL DEFAULT VALIDATOR HINT: ask until VALIDATOR accepts the # prompt_value LABEL DEFAULT VALIDATOR HINT: ask until VALIDATOR accepts the
# answer; the accepted answer is returned in REPLY. # answer; the accepted answer is returned in REPLY.
@@ -27,6 +27,27 @@ prompt_value() {
done done
} }
# prompt_secret LABEL VALIDATOR HINT: like prompt_value for a secret. What is
# typed is not shown (read -s) and a refused answer is never repeated in the
# message. An empty answer is refused; there is no default.
prompt_secret() {
local label="$1" validator="$2" hint="$3" answer
while true; do
printf '%s (input is hidden): ' "$label" >&2
IFS= read -rs answer || {
printf '\n' >&2
die "no input available for '$label'"
}
printf '\n' >&2 # the newline that hidden input did not echo
answer="$(trim "$answer")"
if [[ -n $answer ]] && "$validator" "$answer"; then
REPLY="$answer"
return 0
fi
warn "invalid $label: $hint"
done
}
# prompt_choice LABEL DEFAULT CHOICE...: the answer is returned in REPLY. # prompt_choice LABEL DEFAULT CHOICE...: the answer is returned in REPLY.
prompt_choice() { prompt_choice() {
local label="$1" default="$2" answer local label="$1" default="$2" answer
+2 -2
View File
@@ -18,8 +18,8 @@ repo_body() {
"${PROJECT[name]}" "$description" "$private" "${PROJECT[name]}" "$description" "$private"
return 0 return 0
fi fi
if ((PROJECT[has_github])); then if [[ -n ${PROJECT[license]} ]]; then
extra=',"auto_init":true,"license":"'"$AGPL_LICENSE_KEY"'"' extra=',"auto_init":true,"license":"'"${PROJECT[license]}"'"'
else else
extra=',"auto_init":false' extra=',"auto_init":false'
fi fi
+5
View File
@@ -58,6 +58,11 @@ is_valid_port() {
[[ $1 =~ ^[0-9]{1,5}$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535)) [[ $1 =~ ^[0-9]{1,5}$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535))
} }
# A Gitea license key such as AGPL-3.0 or MIT, or the word none.
is_valid_license() {
[[ -n $1 && ${#1} -le 64 && $1 =~ ^[A-Za-z0-9.+-]+$ ]]
}
# A Go duration such as 10m0s or 8h0m0s, the form Gitea expects. # A Go duration such as 10m0s or 8h0m0s, the form Gitea expects.
is_valid_interval() { is_valid_interval() {
[[ -n $1 && $1 =~ ^([0-9]+h)?([0-9]+m)?([0-9]+s)?$ ]] [[ -n $1 && $1 =~ ^([0-9]+h)?([0-9]+m)?([0-9]+s)?$ ]]
+16 -2
View File
@@ -21,6 +21,16 @@ readonly FAKE_GITHUB_PAT="ghpFAKEtoken1234567890"
# Answers to the prompts: name, description, visibility, Gitea owner, GitHub # Answers to the prompts: name, description, visibility, Gitea owner, GitHub
# yes or no, GitHub owner, directory, plan gate. # yes or no, GitHub owner, directory, plan gate.
readonly ANSWERS_GITHUB=$'my-app\nA test app\n\nTirSystem\ny\nacme-org\n\nn\n' readonly ANSWERS_GITHUB=$'my-app\nA test app\n\nTirSystem\ny\nacme-org\n\nn\n'
# The same, for a public project (the AGPL-3.0 default applies with GitHub).
readonly ANSWERS_GITHUB_PUBLIC=$'my-app
A test app
public
TirSystem
y
acme-org
n
'
readonly ANSWERS_GITEA_ONLY=$'my-app\n\n\nTirSystem\nn\n\nn\n' readonly ANSWERS_GITEA_ONLY=$'my-app\n\n\nTirSystem\nn\n\nn\n'
SHARED_REMOTES="" SHARED_REMOTES=""
@@ -110,12 +120,15 @@ write_gitconfig() {
insteadOf = https://git.example.test/ insteadOf = https://git.example.test/
[url "file://$WORK/remote/"] [url "file://$WORK/remote/"]
insteadOf = ssh://git@git.example.test:10022/ insteadOf = ssh://git@git.example.test:10022/
[url "file://$WORK/remote/"]
insteadOf = ssh://git@git.tirsystem.com:10022/
EOF EOF
} }
# ensure_shared_remotes: build, once per run of the suite, the local bare # ensure_shared_remotes: build, once per run of the suite, the local bare
# repositories that stand in for Gitea (TirSystem/my-app.git, holding the # repositories that stand in for Gitea (TirSystem/my-app.git, holding the
# license commit) and for the framework (a copy of the real one). # license commit), for the framework and for the checklists the framework
# holds as its own submodule (copies of the real ones).
ensure_shared_remotes() { ensure_shared_remotes() {
if [[ -n $SHARED_REMOTES && -d $SHARED_REMOTES ]]; then if [[ -n $SHARED_REMOTES && -d $SHARED_REMOTES ]]; then
return 0 return 0
@@ -123,6 +136,7 @@ ensure_shared_remotes() {
SHARED_REMOTES="$(mktemp -d "${TMPDIR:-/tmp}/repofoundry-remotes.XXXXXX")" SHARED_REMOTES="$(mktemp -d "${TMPDIR:-/tmp}/repofoundry-remotes.XXXXXX")"
mkdir -p "$SHARED_REMOTES/TirSystem" mkdir -p "$SHARED_REMOTES/TirSystem"
git clone -q --bare "$REPO_ROOT/framework" "$SHARED_REMOTES/TirSystem/SQA-QC-Framework.git" git clone -q --bare "$REPO_ROOT/framework" "$SHARED_REMOTES/TirSystem/SQA-QC-Framework.git"
git clone -q --bare "$REPO_ROOT/framework/qc" "$SHARED_REMOTES/TirSystem/SQA-QC-Checklists.git"
git init -q --bare "$SHARED_REMOTES/TirSystem/my-app.git" git init -q --bare "$SHARED_REMOTES/TirSystem/my-app.git"
git init -q "$SHARED_REMOTES/seed" git init -q "$SHARED_REMOTES/seed"
git -C "$SHARED_REMOTES/seed" symbolic-ref HEAD refs/heads/main git -C "$SHARED_REMOTES/seed" symbolic-ref HEAD refs/heads/main
@@ -153,7 +167,7 @@ setup_local_remotes() {
# first, then the now empty directories from the bottom up. # first, then the now empty directories from the bottom up.
remove_workdir() { remove_workdir() {
if [[ -n $WORK && -d $WORK ]]; then if [[ -n $WORK && -d $WORK ]]; then
find "$WORK" \( -type f -o -type p \) -delete find "$WORK" \( -type f -o -type p -o -type l \) -delete
find "$WORK" -depth -type d -exec rmdir {} + find "$WORK" -depth -type d -exec rmdir {} +
fi fi
WORK="" WORK=""
-13
View File
@@ -96,7 +96,6 @@ validate_credentials"
assert_status "$case_name" 1 "$STATUS" assert_status "$case_name" 1 "$STATUS"
assert_contains "$case_name message" "$ERR" "$expected" assert_contains "$case_name message" "$ERR" "$expected"
done <<'EOF' done <<'EOF'
no Gitea token|GITHUB_USER=octo\n|GITEA_TOKEN is missing
token too short|GITEA_TOKEN=short\n|not a valid token token too short|GITEA_TOKEN=short\n|not a valid token
token with a backslash|GITEA_TOKEN=abc\\defgh12345\n|not a valid token token with a backslash|GITEA_TOKEN=abc\\defgh12345\n|not a valid token
bad GitHub token|GITEA_TOKEN=abcdefgh12345\nGITHUB_PAT=bad token\n|GITHUB_PAT bad GitHub token|GITEA_TOKEN=abcdefgh12345\nGITHUB_PAT=bad token\n|GITHUB_PAT
@@ -104,18 +103,6 @@ bad GitHub user|GITEA_TOKEN=abcdefgh12345\nGITHUB_USER=-bad-\n|GITHUB_USER
EOF EOF
} }
test_github_credentials_required_only_when_chosen() {
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/e.env"
run_lib "" "parse_env_file \"$WORK/e.env\" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
echo no-github-ok
ENV_FILE=\"$WORK/e.env\"
require_github_credentials"
assert_contains "Gitea-only .env is valid" "$OUT" "no-github-ok"
assert_status "GitHub credentials missing" 1 "$STATUS"
assert_contains "names the missing key" "$ERR" "GITHUB_PAT is missing"
}
test_validators() { test_validators() {
local fn value expected local fn value expected
while IFS='|' read -r fn value expected; do while IFS='|' read -r fn value expected; do
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env bash
# test-credentials.sh - tests for the credentials that .env does not provide
# and for the .env file of the new project (MIL-005): they are asked without
# echo, the project .env is only written after a yes, owner-only and ignored
# by git, and no token appears anywhere else. Sourced by run-tests.sh.
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
readonly NL=$'\n'
# credentials_input: the answers of a run with no .env at all and GitHub
# chosen: the Gitea token, the details, then the GitHub token and account.
credentials_input() {
printf '%s' "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
}
run_dry_cred() {
run_cli "$1" --config "$WORK/config.env" --env "$WORK/.env"
}
run_apply_cred() {
run_cli "$1" --apply --config "$WORK/config.env" --env "$WORK/.env"
}
# without_env: remove the .env of the fixtures.
without_env() {
rm -f -- "$WORK/.env"
}
# env_mode FILE: the permission bits, empty where the platform has none.
env_mode() {
case "$(uname -s 2>/dev/null || true)" in
MINGW* | MSYS* | CYGWIN*) ;;
*) stat -c '%a' -- "$1" 2>/dev/null || stat -f '%Lp' -- "$1" 2>/dev/null || true ;;
esac
}
# ------------------------------------------------------------ prompt_secret
test_prompt_secret_asks_again_and_never_repeats_the_answer() {
run_lib $'short\n\nlongenoughtoken1\n' \
'prompt_secret "Gitea access token" is_valid_token "needs 8 characters"; echo "[$REPLY]"'
assert_status "valid answer found" 0 "$STATUS"
assert_eq "valid answer returned" "[longenoughtoken1]" "$OUT"
assert_contains "told why" "$ERR" "invalid Gitea access token: needs 8 characters"
assert_not_contains "refused answer not repeated" "$ERR" "short"
assert_contains "says the input is hidden" "$ERR" "(input is hidden)"
}
test_prompt_secret_stops_when_input_ends() {
run_lib "" 'prompt_secret "Gitea access token" is_valid_token "x" </dev/null'
assert_status "end of input" 1 "$STATUS"
assert_contains "message names the credential" "$ERR" "no input available for 'Gitea access token'"
}
# ------------------------------------------------------ collect_credentials
test_collect_credentials_asks_only_what_is_missing() {
run_lib "$FAKE_GITHUB_PAT${NL}octo-user$NL" \
'CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
collect_credentials GITEA_TOKEN GITHUB_PAT GITHUB_USER
printf "%s|%s\n" "${CREDENTIALS[GITHUB_USER]}" "${#SECRET_VALUES[@]}"'
assert_status "asked" 0 "$STATUS"
assert_eq "account name kept, one secret registered" "octo-user|1" "$OUT"
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token"
assert_contains "account asked" "$ERR" "GitHub account name"
assert_not_contains "Gitea token not asked" "$ERR" "Gitea access token"
assert_not_contains "no token shown" "$ERR$OUT" "$FAKE_GITHUB_PAT"
}
test_a_token_that_is_asked_is_registered_as_a_secret() {
run_lib "$FAKE_GITEA_TOKEN$NL" \
'collect_credentials GITEA_TOKEN
warn "the token is giteaFAKEtoken1234567890 here"'
assert_status "asked" 0 "$STATUS"
assert_not_contains "redacted in later messages" "$ERR" "$FAKE_GITEA_TOKEN"
}
test_an_empty_value_in_env_counts_as_not_provided() {
printf 'GITEA_TOKEN=\nGITHUB_USER=\n' >"$WORK/e.env"
run_lib "$FAKE_GITEA_TOKEN$NL" \
'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
collect_credentials GITEA_TOKEN
echo asked-ok'
assert_status "empty value tolerated" 0 "$STATUS"
assert_contains "asked instead" "$ERR" "Gitea access token"
}
test_validate_credentials_no_longer_requires_any() {
printf '# nothing\n' >"$WORK/e.env"
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
echo fine'
assert_status "no credential required" 0 "$STATUS"
assert_eq "no error" "fine" "$OUT"
printf 'GITEA_TOKEN=short\n' >"$WORK/e.env"
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials'
assert_status "a provided bad token is still refused" 1 "$STATUS"
assert_contains "named" "$ERR" "GITEA_TOKEN"
}
# --------------------------------------------------------------- the run
test_env_is_optional_and_the_token_is_asked_without_echo() {
setup_hosts
without_env
run_dry_cred "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
assert_status "dry run without .env" 0 "$STATUS"
assert_contains "token asked" "$ERR" "Gitea access token (input is hidden)"
assert_contains "plan printed" "$OUT" "Plan:"
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "only reads" "$(calls)" "POST"
}
test_a_provided_credential_is_not_asked() {
setup_hosts
run_dry_cred "$ANSWERS_GITHUB"
assert_status "all provided" 0 "$STATUS"
assert_not_contains "no token prompt" "$ERR" "(input is hidden)"
assert_not_contains "no account prompt" "$ERR" "GitHub account name"
}
test_github_credentials_are_asked_only_when_github_is_chosen() {
setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_dry_cred "$ANSWERS_GITEA_ONLY"
assert_status "Gitea only" 0 "$STATUS"
assert_not_contains "no GitHub token asked" "$ERR" "GitHub personal access token"
assert_not_contains "no GitHub account asked" "$ERR" "GitHub account name"
run_dry_cred "$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
assert_status "GitHub chosen" 0 "$STATUS"
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token (input is hidden)"
assert_contains "GitHub account asked" "$ERR" "GitHub account name"
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITHUB_PAT"
}
test_an_invalid_asked_value_is_asked_again_and_never_shown() {
setup_hosts
without_env
run_dry_cred "bad token${NL}$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
assert_status "second answer accepted" 0 "$STATUS"
assert_contains "told it is invalid" "$ERR" "invalid Gitea access token"
assert_not_contains "refused value not shown" "$ERR$OUT" "bad token"
}
test_input_that_ends_stops_before_any_request() {
setup_hosts
without_env
run_dry_cred ""
assert_status "stopped" 1 "$STATUS"
assert_contains "key named" "$ERR" "no input available for 'Gitea access token'"
assert_eq "no request made" "" "$(calls)"
assert_not_contains "no creation report" "$OUT" "This is what exists now"
}
test_asked_tokens_do_not_leak_under_bash_x() {
setup_hosts
without_env
STATUS=0
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" -x "$SCRIPT" \
--config "$WORK/config.env" --env "$WORK/.env" <<<"$(credentials_input)" \
>"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$?
assert_status "run under bash -x" 0 "$STATUS"
assert_not_contains "no Gitea token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITHUB_PAT"
}
# ---------------------------------------------------------- the project .env
test_the_dry_run_names_the_env_step_and_writes_nothing() {
setup_hosts
run_dry_cred "$ANSWERS_GITHUB"
assert_contains "plan line" "$OUT" "Project .env : you are asked whether to create it (GITEA_TOKEN GITHUB_PAT GITHUB_USER)"
assert_file_missing "no .env" "$WORK/my-app/.env"
}
test_the_project_env_is_not_created_without_a_yes() {
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y$NL$NL"
assert_status "run" 0 "$STATUS"
assert_file_missing "default is no" "$WORK/my-app/.env"
assert_contains "reported" "$OUT" "Project .env : skipped (you declined)"
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y${NL}n$NL"
assert_file_missing "explicit no" "$WORK/my-app/.env"
}
test_the_project_env_holds_only_the_needed_keys_and_is_private() {
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_file_exists ".env created" "$WORK/my-app/.env"
assert_eq "exactly the needed keys" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
assert_eq "owner-only" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
assert_contains "reported with the keys, not the values" "$OUT" "Project .env : created (GITEA_TOKEN GITHUB_PAT GITHUB_USER;"
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in the output" "$OUT$ERR" "$FAKE_GITHUB_PAT"
# Git ignores it without any tracked file changing.
check
if ! git -C "$WORK/my-app" check-ignore -q -- .env; then
fail ".env is not ignored by git"
fi
assert_not_contains "not listed by git status" "$(git -C "$WORK/my-app" status --porcelain)" ".env"
assert_contains "excluded locally" "$(cat "$WORK/my-app/.git/info/exclude")" ".env"
check
if [[ -e $WORK/my-app/.gitignore ]]; then
fail "a .gitignore was written; only .git/info/exclude may change"
fi
# No temporary file is left behind.
assert_eq "no leftover file" "" "$(find "$WORK/my-app" -maxdepth 1 -name '.env.*' -print)"
}
test_the_project_env_without_github_holds_only_the_gitea_token() {
setup_hosts
run_apply_cred "${ANSWERS_GITEA_ONLY}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_eq "one key" "GITEA_TOKEN=$FAKE_GITEA_TOKEN" "$(cat "$WORK/my-app/.env")"
assert_contains "reported" "$OUT" "Project .env : created (GITEA_TOKEN;"
}
test_asked_credentials_are_what_the_project_env_holds() {
setup_hosts
without_env
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_eq "the asked values" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
}
test_no_token_is_in_any_file_but_the_project_env() {
setup_hosts
without_env
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
local hits
# The new project (with its .git folder), the script's temporary directory
# and its output; the stub curl's own request log is not part of the product.
hits="$(grep -rIl -F -e "$FAKE_GITEA_TOKEN" -e "$FAKE_GITHUB_PAT" "$WORK/my-app" "$WORK/tmp" "$WORK/out.txt" "$WORK/err.txt" 2>/dev/null |
grep -v -e '/my-app/\.env$' || true)"
assert_eq "only the project .env holds a token" "" "$hits"
}
test_an_existing_env_is_kept_unless_the_maintainer_says_replace() {
setup_hosts
mkdir -p "$WORK/my-app"
printf 'keep\n' >"$WORK/my-app/.env"
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}n$NL"
assert_status "declined replacing" 0 "$STATUS"
assert_eq "unchanged" "keep" "$(cat "$WORK/my-app/.env")"
assert_contains "reported" "$OUT" "Project .env : kept (the existing .env was left as it was)"
remove_workdir
new_workdir
setup_hosts
mkdir -p "$WORK/my-app"
printf 'keep\n' >"$WORK/my-app/.env"
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}y$NL"
assert_status "agreed to replace" 0 "$STATUS"
assert_contains "replaced" "$(cat "$WORK/my-app/.env")" "GITEA_TOKEN=$FAKE_GITEA_TOKEN"
assert_eq "private after replacing" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
}
test_a_tracked_env_is_never_written() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf 'tracked\n' >"$WORK/p/.env"
git -C "$WORK/p" add .env
git -C "$WORK/p" -c user.name=t -c user.email=t@example.test commit -q -m init
run_lib "y$NL" \
'PROJECT[directory]="'"$WORK"'/p"; PROJECT[has_github]=0
CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
init_steps
create_env_file
echo "${STEP_STATUS["Project .env"]}"'
assert_status "run" 0 "$STATUS"
assert_eq "skipped" "skipped" "$OUT"
assert_eq "unchanged" "tracked" "$(cat "$WORK/p/.env")"
assert_contains "says why" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "skipped"
}
test_exclude_is_added_once_and_keeps_the_existing_entries() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf 'build/' >"$WORK/p/.git/info/exclude" # no trailing newline
run_lib "" \
'exclude_env_file "'"$WORK"'/p"
exclude_env_file "'"$WORK"'/p"
echo done'
assert_status "run" 0 "$STATUS"
local exclude
exclude="$(cat "$WORK/p/.git/info/exclude")"
assert_contains "old entry kept" "$exclude" "build/"
assert_eq "entry added once" "1" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude")"
assert_eq "old entry still on its own line" "1" "$(grep -c '^build/$' "$WORK/p/.git/info/exclude")"
}
test_exclude_does_nothing_when_env_is_already_ignored() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf '.env\n' >"$WORK/p/.gitignore"
run_lib "" 'exclude_env_file "'"$WORK"'/p"; echo done'
assert_status "run" 0 "$STATUS"
assert_eq "exclude file untouched" "0" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude" || true)"
}
+23 -9
View File
@@ -31,7 +31,7 @@ test_dry_run_prints_the_plan_and_only_reads() {
setup_hosts setup_hosts
run_dry "$ANSWERS_GITHUB" run_dry "$ANSWERS_GITHUB"
assert_status "dry run" 0 "$STATUS" assert_status "dry run" 0 "$STATUS"
assert_contains "Gitea plan" "$OUT" "Gitea repository : create (private) with the AGPL-3.0 license https://git.example.test/TirSystem/my-app" assert_contains "Gitea plan" "$OUT" "Gitea repository : create (private), empty https://git.example.test/TirSystem/my-app"
assert_contains "GitHub plan" "$OUT" "GitHub repository : create (private), empty https://github.com/acme-org/my-app" assert_contains "GitHub plan" "$OUT" "GitHub repository : create (private), empty https://github.com/acme-org/my-app"
assert_contains "mirror plan" "$OUT" "Push mirror : Gitea -> GitHub every 10m0s" assert_contains "mirror plan" "$OUT" "Push mirror : Gitea -> GitHub every 10m0s"
assert_contains "origin plan" "$OUT" "Local origin : will use SSH (the SSH test passed)" assert_contains "origin plan" "$OUT" "Local origin : will use SSH (the SSH test passed)"
@@ -148,14 +148,18 @@ test_github_owner_must_be_a_member() {
assert_contains "pending message" "$ERR" "membership of the GitHub organization 'acme-org' is not active" assert_contains "pending message" "$ERR" "membership of the GitHub organization 'acme-org' is not active"
} }
test_license_must_be_offered_when_github_is_chosen() { test_license_must_be_offered_when_a_public_project_has_github() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]' prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
run_apply "$ANSWERS_GITHUB" run_apply "$ANSWERS_GITHUB_PUBLIC"
assert_status "no AGPL" 1 "$STATUS" assert_status "no AGPL" 1 "$STATUS"
assert_contains "message" "$ERR" "does not offer the AGPL-3.0 license" assert_contains "message" "$ERR" "does not offer the AGPL-3.0 license"
assert_not_contains "nothing created" "$(calls)" "POST" assert_not_contains "nothing created" "$(calls)" "POST"
# Without GitHub no license is needed, so the same server is fine. # Without GitHub, or for a private project, no license is needed, so the same server is fine.
setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
run_dry "$ANSWERS_GITHUB"
assert_status "private with GitHub" 0 "$STATUS"
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]' prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
@@ -215,8 +219,7 @@ test_apply_sends_the_right_request_bodies() {
bodies="$(cat "$WORK/curl.bodies")" bodies="$(cat "$WORK/curl.bodies")"
assert_contains "GitHub name" "$bodies" '"name":"my-app"' assert_contains "GitHub name" "$bodies" '"name":"my-app"'
assert_contains "GitHub is created empty" "$bodies" '"private":true,"auto_init":false}' assert_contains "GitHub is created empty" "$bodies" '"private":true,"auto_init":false}'
assert_contains "Gitea gets the license" "$bodies" '"license":"AGPL-3.0"' assert_not_contains "a private project gets no license" "$bodies" '"license"'
assert_contains "Gitea is initialised with it" "$bodies" '"auto_init":true'
assert_contains "default branch" "$bodies" '"default_branch":"main"' assert_contains "default branch" "$bodies" '"default_branch":"main"'
assert_contains "description" "$bodies" '"description":"A test app"' assert_contains "description" "$bodies" '"description":"A test app"'
assert_contains "mirror target without credentials" "$bodies" '"remote_address":"https://github.com/acme-org/my-app.git"' assert_contains "mirror target without credentials" "$bodies" '"remote_address":"https://github.com/acme-org/my-app.git"'
@@ -239,6 +242,17 @@ test_apply_never_prints_or_passes_a_token() {
assert_contains "GitHub token in the private config" "$(cat "$WORK/curl.config")" "Authorization: Bearer $FAKE_GITHUB_PAT" assert_contains "GitHub token in the private config" "$(cat "$WORK/curl.config")" "Authorization: Bearer $FAKE_GITHUB_PAT"
} }
test_apply_sends_the_license_for_a_public_project_with_github() {
setup_hosts
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
local bodies
bodies="$(cat "$WORK/curl.bodies")"
assert_contains "Gitea gets the license" "$bodies" '"license":"AGPL-3.0"'
assert_contains "Gitea is initialised with it" "$bodies" '"auto_init":true'
assert_contains "public" "$bodies" '"private":false'
assert_contains "plan names the rule" "$OUT" "with the AGPL-3.0 license (default: GitHub and a public project)"
}
test_apply_with_gitea_only() { test_apply_with_gitea_only() {
setup_hosts setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
@@ -337,7 +351,7 @@ test_gitea_repository_with_only_the_license_can_be_reused() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}' prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}'
prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"LICENSE","type":"file","path":"LICENSE"}]' prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"LICENSE","type":"file","path":"LICENSE"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n' run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "license only" 0 "$STATUS" assert_status "license only" 0 "$STATUS"
assert_not_contains "Gitea repository not created again" "$(calls)" "$GITEA_REPO_CALL" assert_not_contains "Gitea repository not created again" "$(calls)" "$GITEA_REPO_CALL"
assert_eq "mirror created once" "1" "$(calls | grep -c -x -F "$MIRROR_CALL")" assert_eq "mirror created once" "1" "$(calls | grep -c -x -F "$MIRROR_CALL")"
@@ -355,7 +369,7 @@ test_gitea_repository_with_only_the_license_can_be_reused() {
test_reusing_an_empty_gitea_repository_warns_about_the_license() { test_reusing_an_empty_gitea_repository_warns_about_the_license() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":true}' prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":true}'
run_apply "$ANSWERS_GITHUB"$'y\ny\n' run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "reuse empty Gitea repository" 0 "$STATUS" assert_status "reuse empty Gitea repository" 0 "$STATUS"
assert_contains "warning" "$ERR" "the AGPL-3.0 license is not added to it" assert_contains "warning" "$ERR" "the AGPL-3.0 license is not added to it"
} }
@@ -485,7 +499,7 @@ test_a_repository_this_script_created_earlier_can_be_reused() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}' prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}'
prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"README.md","type":"file"},{"name":"LICENSE","type":"file"}]' prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"README.md","type":"file"},{"name":"LICENSE","type":"file"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n' run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "LICENSE and README.md" 0 "$STATUS" assert_status "LICENSE and README.md" 0 "$STATUS"
assert_not_contains "not created again" "$(calls)" "$GITEA_REPO_CALL" assert_not_contains "not created again" "$(calls)" "$GITEA_REPO_CALL"
assert_contains "reported" "$OUT" "Gitea repository : reused" assert_contains "reported" "$OUT" "Gitea repository : reused"
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env bash
# test-launch.sh - tests for MIL-007: the framework's own submodules (qc) are
# fetched, the script starts through a link, and the configuration files are
# --config and --env, else ./config.env and ./.env, else the checkout's.
# Sourced by run-tests.sh.
# shellcheck disable=SC2016,SC2153 # snippet and fixture text is literal on purpose; SCRIPT comes from lib.sh
# make_checkout: a copy of the script's own files with config.env and .env
# beside them, standing in for the checkout; the path is in CHECKOUT.
make_checkout() {
CHECKOUT="$(cd "$WORK" && pwd -P)/checkout"
mkdir -p "$CHECKOUT"
cp -R "$SRC_DIR" "$CHECKOUT/src"
cp "$WORK/config.env" "$CHECKOUT/config.env"
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$CHECKOUT/.env"
}
# make_folder: an empty working folder; the path is in FOLDER.
make_folder() {
FOLDER="$(cd "$WORK" && pwd -P)/folder"
mkdir -p "$FOLDER"
}
# run_from SCRIPT_PATH DIR INPUT ARGS...: run the script with DIR as the
# current folder.
run_from() {
local script="$1" dir="$2" input="$3"
shift 3
STATUS=0
(cd "$dir" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" \
REPOFOUNDRY_SYNC_WAIT=0 GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 \
"$BASH" "$script" "$@" <<<"$input" >"$WORK/out.txt" 2>"$WORK/err.txt") ||
STATUS=$?
OUT="$(cat "$WORK/out.txt")"
ERR="$(cat "$WORK/err.txt")"
}
# --------------------------------------------------- the framework's qc
test_the_framework_checklists_are_fetched() {
setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
local dir="$WORK/project" answers
printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir"
run_apply "$answers"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_file_exists "the framework" "$dir/framework/README.md"
assert_file_exists "qc is filled" "$dir/framework/qc/qc-business-case.md"
assert_eq "no submodule is left uninitialised" "" "$(GIT_CONFIG_GLOBAL="$WORK/gitconfig" git -C "$dir" submodule status --recursive | grep '^-' || true)"
}
test_an_empty_qc_is_filled_by_a_second_run_and_a_complete_one_is_not_changed() {
setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
local dir="$WORK/project" answers
printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir"
run_apply "$answers"$'y\n'
assert_status "first run" 0 "$STATUS"
git_in() { GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" "$@"; }
git_in submodule deinit -f qc >/dev/null 2>&1
assert_file_missing "qc emptied" "$dir/framework/qc/qc-business-case.md"
run_lib "" "setup_temp_dir; init_framework_submodules '$dir'"
assert_status "repair" 0 "$STATUS"
assert_file_exists "qc filled again" "$dir/framework/qc/qc-business-case.md"
local before after
before="$(git_in status --porcelain)"
run_lib "" "setup_temp_dir; init_framework_submodules '$dir'"
after="$(git_in status --porcelain)"
assert_status "second call" 0 "$STATUS"
assert_eq "nothing else changed" "$before" "$after"
}
test_a_failed_qc_fetch_names_the_command_to_run_by_hand() {
setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
local dir="$WORK/project" answers
printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir"
run_apply "$answers"$'y\n'
GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" submodule deinit -f qc >/dev/null 2>&1
# Drop the cached copy of the checklists and refuse local fetches, so they
# cannot be fetched again.
local cache="$dir/.git/modules/framework/modules/qc"
if [[ -d $cache ]]; then
find "$cache" \( -type f -o -type l \) -delete
find "$cache" -depth -type d -exec rmdir {} +
fi
printf '[protocol "file"]\n\tallow = never\n' >>"$WORK/gitconfig"
run_lib "" "setup_temp_dir; init_framework_submodules '$dir'"
assert_status "fetch fails" 1 "$STATUS"
assert_contains "the command" "$ERR" "git submodule update --init --recursive"
assert_not_contains "no token" "$ERR" "$FAKE_GITEA_TOKEN"
}
test_a_framework_without_a_submodule_of_its_own_is_not_a_failure() {
setup_hosts
local dir="$WORK/plain"
mkdir -p "$dir"
git init -q "$dir"
run_lib "" "setup_temp_dir; init_framework_submodules '$dir'"
assert_status "nothing to fetch" 0 "$STATUS"
}
# ------------------------------------------------ the configuration files
test_files_in_the_working_folder_are_used_after_a_yes() {
setup_hosts
make_checkout
make_folder
cp "$WORK/config.env" "$FOLDER/config.env"
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env"
run_from "$SCRIPT" "$FOLDER" $'y\n'"$ANSWERS_GITEA_ONLY"
assert_status "folder files" 0 "$STATUS"
assert_contains "config named" "$OUT" "Config file : $FOLDER/config.env (from the working folder)"
assert_contains "credentials named" "$OUT" "Credentials file: $FOLDER/.env (from the working folder)"
assert_contains "the address is named" "$OUT" "Gitea would be https://git.example.test"
assert_contains "asked" "$ERR" "Use $FOLDER/config.env $FOLDER/.env (y/n) [n]"
}
test_files_in_the_working_folder_are_not_used_without_a_yes() {
setup_hosts
make_checkout
make_folder
cp "$WORK/config.env" "$FOLDER/config.env"
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env"
rm -f "$WORK/curl.calls"
run_from "$SCRIPT" "$FOLDER" $'\n'"$ANSWERS_GITEA_ONLY"
assert_status "default no" 1 "$STATUS"
assert_contains "stopped" "$ERR" "stopped before any request"
assert_eq "no request to any host" "" "$(calls)"
}
test_named_files_win_and_are_not_confirmed() {
setup_hosts
make_checkout
make_folder
cp "$WORK/config.env" "$FOLDER/config.env"
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env"
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_from "$SCRIPT" "$FOLDER" "$ANSWERS_GITEA_ONLY" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "named files" 0 "$STATUS"
assert_contains "named" "$OUT" "Config file : $WORK/config.env (named on the command line)"
assert_not_contains "no confirmation" "$OUT" "The working folder supplies"
}
test_the_checkouts_files_are_used_when_the_folder_has_none() {
setup_hosts
make_checkout
make_folder
run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" "$ANSWERS_GITEA_ONLY"
assert_status "checkout files" 0 "$STATUS"
assert_contains "config named" "$OUT" "Config file : $CHECKOUT/config.env (from the checkout)"
assert_contains "credentials named" "$OUT" "Credentials file: $CHECKOUT/.env (from the checkout)"
assert_not_contains "no confirmation" "$OUT" "The working folder supplies"
}
test_each_file_is_chosen_on_its_own() {
setup_hosts
make_checkout
make_folder
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env"
run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" $'y\n'"$ANSWERS_GITEA_ONLY"
assert_status "mixed" 0 "$STATUS"
assert_contains "config from the checkout" "$OUT" "Config file : $CHECKOUT/config.env (from the checkout)"
assert_contains "credentials from the folder" "$OUT" "Credentials file: $FOLDER/.env (from the working folder)"
assert_contains "asked about the folder file only" "$ERR" "Use $FOLDER/.env (y/n) [n]"
}
test_a_config_file_found_nowhere_stops_before_any_request_and_names_both_places() {
setup_hosts
make_checkout
make_folder
rm -f "$CHECKOUT/config.env" "$WORK/curl.calls"
run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" "$ANSWERS_GITEA_ONLY"
assert_status "no config.env anywhere" 1 "$STATUS"
assert_contains "working folder named" "$ERR" "working folder ($FOLDER)"
assert_contains "checkout named" "$ERR" "checkout ($CHECKOUT)"
assert_contains "the option" "$ERR" "--config FILE"
assert_eq "no request to any host" "" "$(calls)"
}
test_a_credentials_file_found_nowhere_means_the_token_is_asked() {
setup_hosts
make_checkout
make_folder
rm -f "$CHECKOUT/.env"
run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" "$FAKE_GITEA_TOKEN"$'\n'"$ANSWERS_GITEA_ONLY"
assert_status "token asked" 0 "$STATUS"
assert_contains "named as none" "$OUT" "Credentials file: (none) (none found; a missing credential is asked)"
assert_not_contains "token never shown" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
}
# ----------------------------------------------------------- a link to it
test_the_script_runs_through_a_link_and_creates_the_project_in_the_current_folder() {
setup_hosts
make_checkout
make_folder
local bin="$WORK/linkbin"
mkdir -p "$bin"
MSYS=winsymlinks:nativestrict ln -s "$CHECKOUT/src/create-project.sh" "$bin/repo-foundry" 2>/dev/null || true
if [[ ! -L $bin/repo-foundry ]]; then
printf 'skipped: this shell cannot make symbolic links\n'
return 0
fi
run_from "$bin/repo-foundry" "$FOLDER" "" --version
assert_status "version through the link" 0 "$STATUS"
assert_contains "found its files" "$OUT" "RepoFoundry"
run_from "$bin/repo-foundry" "$FOLDER" "$ANSWERS_GITEA_ONLY"$'y\n' --apply
assert_status "apply through the link" 0 "$STATUS"
assert_contains "the checkout's files" "$OUT" "/checkout/config.env (from the checkout)"
assert_file_exists "the project is in the current folder" "$FOLDER/my-app/.git"
assert_file_missing "not in the checkout" "$CHECKOUT/my-app"
}
test_a_link_to_a_link_is_followed() {
setup_hosts
make_checkout
make_folder
local bin="$WORK/linkbin"
mkdir -p "$bin"
MSYS=winsymlinks:nativestrict ln -s "$CHECKOUT/src/create-project.sh" "$bin/first" 2>/dev/null || true
[[ -L $bin/first ]] || {
printf 'skipped: this shell cannot make symbolic links\n'
return 0
}
(cd "$bin" && MSYS=winsymlinks:nativestrict ln -s first second)
run_from "$bin/second" "$FOLDER" "" --version
assert_status "second link" 0 "$STATUS"
}
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# test-license.sh - tests for the project license (MIL-006): PROJECT_LICENSE
# in config.env, the default (AGPL-3.0 only for a public project with GitHub),
# "none", invalid values and a license the server does not offer. Sourced by
# run-tests.sh.
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
readonly ANSWERS_GITEA_ONLY_PUBLIC=$'my-app\n\npublic\nTirSystem\nn\n\nn\n'
readonly MIT_ROUTE='GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"},{"key":"AGPL-3.0","name":"AGPL-3.0"}]'
# use_license LINE: add a line to config.env and make the server offer MIT.
use_license() {
printf '%s\n' "$1" >>"$WORK/config.env"
prepend_route "$MIT_ROUTE"
}
# gitea_only_env: a Gitea-only run needs no GitHub credentials.
gitea_only_env() {
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
}
# ------------------------------------------------------------- key is set
test_a_license_in_config_is_used_with_github_for_a_private_project() {
setup_hosts
use_license "PROJECT_LICENSE=MIT"
run_apply "$ANSWERS_GITHUB"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_contains "Gitea gets MIT" "$(cat "$WORK/curl.bodies")" '"license":"MIT"'
assert_not_contains "no AGPL" "$(cat "$WORK/curl.bodies")" "AGPL"
assert_contains "plan" "$OUT" "create (private) with the MIT license (from config.env)"
assert_contains "summary" "$OUT" "License : MIT (from config.env)"
}
test_a_license_in_config_is_used_without_github() {
setup_hosts
gitea_only_env
use_license "PROJECT_LICENSE=MIT"
run_apply "$ANSWERS_GITEA_ONLY"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_contains "Gitea gets MIT" "$(cat "$WORK/curl.bodies")" '"license":"MIT"'
assert_contains "initialised with it" "$(cat "$WORK/curl.bodies")" '"auto_init":true'
assert_not_contains "no GitHub call" "$(calls)" "api.github.com"
assert_contains "the server is asked" "$(calls)" "/licenses"
}
test_the_license_key_is_taken_in_any_case_for_none() {
setup_hosts
use_license "PROJECT_LICENSE=NONE"
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_not_contains "no license" "$(cat "$WORK/curl.bodies")" '"license"'
}
# ------------------------------------------------------------------- none
test_none_gives_no_license_even_for_a_public_project_with_github() {
setup_hosts
use_license "PROJECT_LICENSE=none"
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_not_contains "no license" "$(cat "$WORK/curl.bodies")" '"license"'
assert_not_contains "no license lookup" "$(calls)" "/licenses"
assert_contains "plan" "$OUT" "create (public), empty"
assert_contains "summary" "$OUT" "License : none (from config.env)"
}
# ----------------------------------------------------------------- absent
test_without_the_key_a_public_project_with_github_gets_agpl() {
setup_hosts
run_dry "$ANSWERS_GITHUB_PUBLIC"
assert_status "dry run" 0 "$STATUS"
assert_contains "plan" "$OUT" "with the AGPL-3.0 license (default: GitHub and a public project)"
assert_contains "summary" "$OUT" "License : AGPL-3.0 (default: GitHub and a public project)"
}
test_without_the_key_a_private_project_with_github_gets_no_license() {
setup_hosts
run_dry "$ANSWERS_GITHUB"
assert_status "dry run" 0 "$STATUS"
assert_contains "summary" "$OUT" "License : none (no default for a private project)"
assert_not_contains "no license lookup" "$(calls)" "/licenses"
}
test_without_the_key_a_project_without_github_gets_no_license() {
setup_hosts
gitea_only_env
run_dry "$ANSWERS_GITEA_ONLY_PUBLIC"
assert_status "dry run" 0 "$STATUS"
assert_contains "summary" "$OUT" "License : none"
assert_not_contains "no marker" "$OUT" "License : none ("
assert_not_contains "no license lookup" "$(calls)" "/licenses"
}
# ---------------------------------------------------- empty and invalid
test_an_empty_license_stops_before_any_request() {
setup_hosts
printf 'PROJECT_LICENSE=\n' >>"$WORK/config.env"
run_dry "$ANSWERS_GITHUB"
assert_status "empty" 1 "$STATUS"
assert_contains "key named" "$ERR" "PROJECT_LICENSE in"
assert_contains "says empty" "$ERR" "is empty"
assert_eq "no request to any host" "" "$(calls)"
}
test_an_invalid_license_stops_before_any_request_and_is_never_asked() {
local value
for value in 'bad license' 'MIT/2' 'MIT;rm' "$(printf 'a%.0s' {1..65})"; do
remove_workdir
new_workdir
setup_hosts
printf 'PROJECT_LICENSE=%s\n' "$value" >>"$WORK/config.env"
run_dry "$ANSWERS_GITHUB"
assert_status "invalid '$value'" 1 "$STATUS"
assert_contains "key named" "$ERR" "PROJECT_LICENSE in"
assert_eq "no request to any host" "" "$(calls)"
assert_not_contains "never asked" "$ERR" "License ("
done
}
# ------------------------------------------------------ not offered
test_a_license_the_server_does_not_offer_stops_before_anything_is_created() {
setup_hosts
use_license "PROJECT_LICENSE=Zlib"
run_apply "$ANSWERS_GITHUB"$'y\n'
assert_status "not offered" 1 "$STATUS"
assert_contains "license named" "$ERR" "does not offer the Zlib license"
assert_not_contains "nothing created" "$(calls)" "POST"
}
# ------------------------------------------------------- never asked
test_the_license_is_never_asked() {
setup_hosts
run_dry "$ANSWERS_GITHUB_PUBLIC"
assert_not_contains "no license prompt" "$ERR" "icense ("
assert_not_contains "no license prompt, any case" "$ERR" "License:"
remove_workdir
new_workdir
setup_hosts
use_license "PROJECT_LICENSE=MIT"
run_dry "$ANSWERS_GITHUB"
assert_not_contains "no license prompt with the key" "$ERR" "icense ("
}
# ------------------------------------------- mirror and local history
test_the_license_file_reaches_the_local_project_without_github() {
setup_hosts
gitea_only_env
use_license "PROJECT_LICENSE=MIT"
local dir="$WORK/project" answers
printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir"
run_apply "$answers"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_eq "the Gitea license commit is the whole history" "1" "$(GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir" rev-list --count HEAD)"
assert_file_exists "LICENSE from Gitea" "$dir/LICENSE"
}
+1 -1
View File
@@ -12,7 +12,7 @@
# LOCAL_ANSWERS (kept in a variable because $(...) would drop the last newline). # LOCAL_ANSWERS (kept in a variable because $(...) would drop the last newline).
local_answers() { local_answers() {
if [[ $2 == y ]]; then if [[ $2 == y ]]; then
printf -v LOCAL_ANSWERS 'my-app\nA test app\n\nTirSystem\ny\nacme-org\n%s\n%s\n' "$1" "$3" printf -v LOCAL_ANSWERS 'my-app\nA test app\npublic\nTirSystem\ny\nacme-org\n%s\n%s\n' "$1" "$3"
else else
printf -v LOCAL_ANSWERS 'my-app\n\n\nTirSystem\nn\n%s\n%s\n' "$1" "$3" printf -v LOCAL_ANSWERS 'my-app\n\n\nTirSystem\nn\n%s\n%s\n' "$1" "$3"
fi fi
+1 -1
View File
@@ -205,7 +205,7 @@ test_summary_marks_the_values_from_config_env() {
assert_contains "name" "$OUT" "Repository : my-app (from config.env) (private (from config.env))" assert_contains "name" "$OUT" "Repository : my-app (from config.env) (private (from config.env))"
assert_contains "description" "$OUT" "Description : A test app (from config.env)" assert_contains "description" "$OUT" "Description : A test app (from config.env)"
assert_contains "Gitea" "$OUT" "/TirSystem/my-app (from config.env)" assert_contains "Gitea" "$OUT" "/TirSystem/my-app (from config.env)"
assert_contains "GitHub" "$OUT" "(AGPL license applied) (from config.env)" assert_contains "GitHub" "$OUT" "/acme-org/my-app (from config.env)"
assert_contains "directory" "$OUT" "Directory : ./my-app (from config.env)" assert_contains "directory" "$OUT" "Directory : ./my-app (from config.env)"
assert_contains "plan gate" "$OUT" "Plan gate : no (from config.env)" assert_contains "plan gate" "$OUT" "Plan gate : no (from config.env)"
} }
+11 -9
View File
@@ -19,10 +19,10 @@ test_full_run_with_github() {
after="$(listing)" after="$(listing)"
assert_status "full run" 0 "$STATUS" assert_status "full run" 0 "$STATUS"
assert_contains "dry run" "$OUT" "Dry run: nothing was created" assert_contains "dry run" "$OUT" "Dry run: nothing was created"
assert_contains "plan" "$OUT" "create (private) with the AGPL-3.0 license" assert_contains "plan" "$OUT" "create (private), empty"
assert_contains "Gitea link derived from config" "$OUT" "https://git.example.test/TirSystem/my-app" assert_contains "Gitea link derived from config" "$OUT" "https://git.example.test/TirSystem/my-app"
assert_contains "GitHub link uses the chosen organization" "$OUT" "https://github.com/acme-org/my-app" assert_contains "GitHub link uses the chosen organization" "$OUT" "https://github.com/acme-org/my-app"
assert_contains "AGPL noted" "$OUT" "AGPL license applied" assert_contains "no default license for a private project" "$OUT" "License : none (no default for a private project)"
assert_contains "credential state" "$OUT" "GITEA_TOKEN set, GITHUB_PAT set" assert_contains "credential state" "$OUT" "GITEA_TOKEN set, GITHUB_PAT set"
assert_not_contains "no Gitea token in output" "$OUT$ERR" "$FAKE_GITEA_TOKEN" assert_not_contains "no Gitea token in output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in output" "$OUT$ERR" "$FAKE_GITHUB_PAT" assert_not_contains "no GitHub token in output" "$OUT$ERR" "$FAKE_GITHUB_PAT"
@@ -47,12 +47,12 @@ test_full_run_without_github() {
assert_contains "plan says GitHub is not used" "$OUT" "GitHub repository : not used" assert_contains "plan says GitHub is not used" "$OUT" "GitHub repository : not used"
} }
test_github_chosen_without_credentials_fails() { test_github_chosen_without_credentials_stops_when_input_ends() {
write_fixtures write_fixtures
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env" run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "missing GitHub credentials" 1 "$STATUS" assert_status "missing GitHub credentials, no answer" 1 "$STATUS"
assert_contains "names the key" "$ERR" "GITHUB_PAT is missing" assert_contains "names the credential" "$ERR" "no input available for 'GitHub personal access token'"
assert_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN" assert_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)" assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
} }
@@ -201,13 +201,15 @@ test_default_files_are_in_the_project_root() {
cp "$WORK/config.env" "$WORK/project/config.env" cp "$WORK/config.env" "$WORK/project/config.env"
cp "$WORK/.env" "$WORK/project/.env" cp "$WORK/.env" "$WORK/project/.env"
STATUS=0 STATUS=0
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ (cd "$WORK/project" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \
<<<"$ANSWERS_GITHUB" >"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$? <<<"$ANSWERS_GITHUB" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$?
assert_status "run with the default files" 0 "$STATUS" assert_status "run with the default files" 0 "$STATUS"
assert_contains "found config.env in the project root" "$(cat "$WORK/out.txt")" "https://git.example.test/TirSystem/my-app" assert_contains "found config.env in the project root" "$(cat "$WORK/out.txt")" "https://git.example.test/TirSystem/my-app"
# Run from another directory: the defaults follow the script, not the cwd. # Run from another folder that holds no files of its own: the checkout's
# files are used; the defaults follow the script, not the current folder.
mkdir -p "$WORK/elsewhere"
STATUS=0 STATUS=0
(cd "$WORK" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ (cd "$WORK/elsewhere" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \
<<<"$ANSWERS_GITEA_ONLY" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$? <<<"$ANSWERS_GITEA_ONLY" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$?
assert_status "run from another directory" 0 "$STATUS" assert_status "run from another directory" 0 "$STATUS"
} }