Merge pull request 'Implement MIL-006 and MIL-007: project license, qc submodules, link start, default configuration files' (#54) from mil-006-license into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 3s

Reviewed-on: #54
This commit was merged in pull request #54.
This commit is contained in:
2026-10-07 09:03:19 +02:00
37 changed files with 917 additions and 135 deletions
+82 -10
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
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
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
are accepted, and anything else stops the run with a message that names the key
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
cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL)
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 |
| `PROJECT_DIRECTORY` | local directory | not empty, not starting with `-` |
| `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
`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.
- `USE_GITHUB=no` skips the GitHub owner and every GitHub step; a
`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
directory, `core.hooksPath` and replacing a template file.
- 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
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
src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
@@ -171,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
```
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,
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
@@ -179,8 +250,8 @@ with read-only requests and prints a plan:
```text
Plan:
Gitea repository : create (private) with the AGPL-3.0 license https://git.example.org/Team/my-app
GitHub repository : create (private), empty https://github.com/acme/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 (public), empty https://github.com/acme/my-app
Push mirror : Gitea -> GitHub every 10m0s
Local project : create ./my-app (new directory), git on main, no commit
Local origin : will use SSH (the SSH test passed)
@@ -193,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:
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;
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
@@ -207,10 +278,11 @@ hooks refuse commits on `main`.
### Choices
- **GitHub or not.** Choosing GitHub also applies the AGPL-3.0 license to the
Gitea repository (so it is not empty) and sets up the mirror. Without GitHub
the Gitea repository is empty and has no license, and `GITHUB_PAT` is not
needed.
- **GitHub or not.** Choosing GitHub sets up the mirror. For a public project it
also applies the AGPL-3.0 license to the Gitea repository (so it is not
empty), unless `PROJECT_LICENSE` says otherwise. A private project with
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
may be a user or an organization. `GITHUB_USER` is only the suggested default
for the GitHub owner prompt; it identifies who authenticates.
+6
View File
@@ -46,6 +46,12 @@ GITEA_API_URL=https://<your gitea instance>/api/v1
#PROJECT_DIRECTORY=./my-project
#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;
# it is added to the new project as a submodule over SSH.
# Default: TirSystem/SQA-QC-Framework.
+1 -1
View File
@@ -24,7 +24,7 @@ document of a type. `Primary File` may contain a glob (e.g.
| DCD | Design Class Diagram | docs/dcd.md | 004 |
| DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 029 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 030 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
## Languages
+5 -4
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Objectives 2, 5 and 10, two scope items and success criterion 10: the AGPL-3.0 default needs GitHub and a public project; the framework's own submodules (qc) are fetched | [1cd27f7] |
| 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-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] |
---
@@ -44,7 +44,7 @@ One repeatable, reviewed procedure gives every new project the same secure basel
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`.
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
@@ -58,7 +58,7 @@ One repeatable, reviewed procedure gives every new project the same secure basel
- 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.
- Starting through a command link: the script finds its own files from the link, and the new project lands in the folder it was started in.
- 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`).
@@ -101,7 +101,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
| 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 the checkout's `config.env` and `.env` and creates the project under that folder | Test run through a link; the README example run once |
| 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
@@ -154,3 +154,4 @@ Proceed — the procedure is small, well bounded and removes a repeated, securit
[SA-001]: ./stakeholder-analysis.md
[UCD-001]: ./use-case-diagram.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+17 -6
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | ProjectRequest carries the license that applies (from DCD-001) | [d773fa9] |
| 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] |
---
@@ -33,7 +33,7 @@ enum Visibility {
}
class ProjectCreator <<controller>> {
+startProjectCreation(checkout : Checkout, workingFolder : WorkingFolder, configPath : Path [0..1], envPath : Path [0..1]) : PromptSet
+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 {
@@ -83,6 +83,7 @@ class SummaryReport {
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 {
@@ -92,6 +93,12 @@ class Checkout {
}
class WorkingFolder {
-path : Path
+configFile() : Path [0..1]
+envFile() : Path [0..1]
}
class ConfigFiles {
-configFile : Path
-envFile : Path
}
class Run {
-isApply : Boolean
@@ -186,6 +193,8 @@ class Summary {
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
@@ -249,9 +258,10 @@ Repository "0..*" --> "1" Visibility
| 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, and starts the run. | none | `resolveCheckout`, `currentFolder`, `startFromWorkingFolder` |
| `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. | `path` | none |
| `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` |
@@ -291,9 +301,10 @@ Repository "0..*" --> "1" Visibility
| Method signature | Operation Contract / SD message |
| --- | --- |
| `ProjectCreator.startProjectCreation(checkout, workingFolder, configPath, envPath) : PromptSet` | [OC-001] `startProjectCreation`; [SD-001] `startProjectCreation()`; [SD-002] `startProjectCreation(checkout, workingFolder, ...)` |
| `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` |
@@ -362,5 +373,5 @@ SOLID check: no class has more than one reason to change (one host API, one kind
[SD-001]: ./uc-001/sd.md
[MIL-005]: ./milestones/mil-005-credentials.md
[DICT-001]: ./dictionary.md
[d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+5 -4
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | LicenseFile definition no longer tied to GitHub | [d773fa9] |
| 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] |
---
@@ -49,8 +49,9 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
- 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.
- 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
have no PO term: they are system concepts, not domain concepts, and are not
- `Run`, `ToolCheck`, `PreflightResult`, `PromptSet` and `ConfigFiles` (the two
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.
- `InstallResult` and the enumeration `Visibility` appear only in [DCD-001]:
`InstallResult` carries the three results of one operation, and `Visibility`
@@ -67,5 +68,5 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
[DM-002]: ./domain-model.md
[OC-001]: ./uc-001/oc.md
[DCD-001]: ./uc-001/dcd.md
[d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+5 -2
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | License: the AGPL-3.0 default needs GitHub and a public project | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added Command Link, Checkout and Working Folder (from DM-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] |
---
@@ -128,6 +128,7 @@ 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
```
@@ -155,7 +156,7 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
| 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 and under which the new project is created by default | path | [UC-002] step 2 "working folder" |
| 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" |
## Association Table
@@ -189,6 +190,7 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
| 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
@@ -206,3 +208,4 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
[DICT-001]: ./dictionary.md
[DM-001]: ./uc-001/dm.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+4 -4
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [d773fa9] |
| 2026-10-07 | Proposed | 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 | 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] |
---
@@ -75,7 +75,7 @@ The license that applies is resolved in this order: `PROJECT_LICENSE` when set (
| 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 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 | |
| 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 | |
---
@@ -84,5 +84,5 @@ The license that applies is resolved in this order: `PROJECT_LICENSE` when set (
[UC-001]: ../uc-001/uc.md
[DCD-001]: ../uc-001/dcd.md
[MIL-004]: ./mil-004-configurable-details.md
[d773fa9]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/d773fa91df5a54090254e12e074880fb6526a9ff
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[be759e3]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/be759e38326eac2b11e65a2b582b2431186e338b
@@ -9,8 +9,8 @@
## 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 | Added UC-002 and US-002 (global command); tasks 2 and 3 trace to UC-002 | [1cd27f7] |
| 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] |
---
@@ -22,7 +22,7 @@ Decide whether a new project holds the whole framework, including the `qc` check
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 checkout and point to them with `--config` and `--env` or place them where the script reads them, 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.
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
@@ -36,6 +36,8 @@ Decide whether a new project holds the whole framework, including the `qc` check
| 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
@@ -68,9 +70,9 @@ Decide whether a new project holds the whole framework, including the `qc` check
| # | 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, and keep the current folder as the base of the default directory. Errors name the folder or path looked in. Steps 3 to 6 and extensions 4a and 4b 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, `--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 every case | `qc` filled after a run and after a rerun, nested fetch failure, framework without a submodule, start through a symlink from another folder. | No | |
| 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 | |
---
@@ -79,4 +81,5 @@ Decide whether a new project holds the whole framework, including the `qc` check
[UC-001]: ../uc-001/uc.md
[UC-002]: ../uc-002/uc.md
[MIL-003]: ./mil-003-scaffold-and-release.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[be759e3]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/be759e38326eac2b11e65a2b582b2431186e338b
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
@@ -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
+13 -12
View File
@@ -24,7 +24,7 @@ updated whenever an artifact instance is created or reviewed.
| 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], [MIL-005], [MIL-006], [MIL-007], [US-001], [UCD-001] | [RC-010], [RC-018], [RC-020], [RC-022] |
| [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] |
| [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] |
@@ -33,24 +33,24 @@ updated whenever an artifact instance is created or reviewed.
| [MIL-004] | MIL | [BC-001], [PP-001] | [US-001] | [RC-018], [RC-019] |
| [MIL-005] | MIL | [BC-001], [PP-001] | [US-001] | [RC-020] |
| [MIL-006] | MIL | [BC-001], [PP-001] | [US-001] | [RC-022] |
| [MIL-007] | MIL | [BC-001], [PP-001] | [US-001], [UC-002] | [RC-022] |
| [MIL-007] | MIL | [BC-001], [PP-001] | [US-001], [UC-002] | [RC-022], [RC-029] |
| [UCD-001] | UCD | [BC-001], [SA-001] | [US-001], [UC-001], [UC-002] | [RC-009], [RC-023] |
| [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] |
| [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] |
| [UC-001] | UC | [UCD-001], [US-001], [SA-001] | [SSD-001], [DM-001] | [RC-002], [RC-020], [RC-023] |
| [UC-002] | UC | [UCD-001], [US-001], [SA-001], [BC-001] | [SSD-002], [DM-003] | [RC-023] |
| [SSD-002] | SSD | [UC-002], [DM-003] | [OC-002] | [RC-024] |
| [DM-003] | DM | [UC-002], [UCD-001], [SSD-002], [DICT-001], [DM-001] | [OC-002], [DCD-003] | [RC-025] |
| [OC-002] | OC | [SSD-002], [DM-003] | [SD-002] | [RC-026] |
| [SD-002] | SD | [OC-002], [DCD-003] | [DCD-003] | [RC-027] |
| [DCD-003] | DCD | [DM-003], [SD-002], [DICT-001], [UC-002], [DCD-001] | [DCD-002] | [RC-028] |
| [UC-002] | UC | [UCD-001], [US-001], [SA-001], [BC-001] | [SSD-002], [DM-003] | [RC-023], [RC-029] |
| [SSD-002] | SSD | [UC-002], [DM-003] | [OC-002] | [RC-024], [RC-029] |
| [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] |
| [DICT-001] | DICT | [BC-001], [SA-001], [DM-001], [DM-002] | [OC-001], [SD-001] | [RC-008], [RC-020], [RC-025] |
| [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] |
| [DCD-002] | DCD | [DCD-001], [DCD-003], [DM-002], [DICT-001] | - | [RC-021], [RC-028], [RC-029] |
## Coverage Notes
@@ -80,6 +80,7 @@ updated whenever an artifact instance is created or reviewed.
[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
+20 -6
View File
@@ -10,6 +10,7 @@
| 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] |
---
@@ -24,6 +25,7 @@ Covers [UC-002]. Adds the classes that start the script through a command link.
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 {
@@ -33,14 +35,22 @@ class Checkout {
}
class WorkingFolder {
-path : Path
+configFile() : Path [0..1]
+envFile() : Path [0..1]
}
class ConfigFiles {
-configFile : Path
-envFile : Path
}
class ProjectCreator {
+startProjectCreation(checkout : Checkout, workingFolder : WorkingFolder, configPath : Path [0..1], envPath : Path [0..1]) : PromptSet
+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
@@ -50,9 +60,10 @@ Run "1" *-- "1" WorkingFolder
| 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, and starts the run. | none | `resolveCheckout`, `currentFolder`, `startFromWorkingFolder` |
| `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. | `path` | none |
| `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.
@@ -63,8 +74,10 @@ The concept Command Link has no class: it is a link the Maintainer makes with th
| `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 |
| `Checkout.configFile() : Path`, `Checkout.envFile() : Path` | [SD-002] `startProjectCreation`; P5 |
| `ProjectCreator.startProjectCreation(checkout, workingFolder, configPath, envPath) : PromptSet` | [SD-002] `startProjectCreation`; P4, P5, P6. Replaces the signature of [DCD-001] by adding `checkout`, `workingFolder`, `configPath` and `envPath` |
| `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
@@ -75,7 +88,7 @@ The concept Command Link has no class: it is a link the Maintainer makes with th
## Dependency Check
`Launcher` depends on `ProjectCreator`, `Checkout` and `WorkingFolder`; none of them depends on `Launcher`, so no cycle is added.
`Launcher` depends on `ProjectCreator`, `Checkout`, `WorkingFolder` and `ConfigFiles`; none of them depends on `Launcher`, so no cycle is added.
---
@@ -86,3 +99,4 @@ The concept Command Link has no class: it is a link the Maintainer makes with th
[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
+5 -1
View File
@@ -10,6 +10,7 @@
| 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] |
---
@@ -46,6 +47,7 @@ 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
```
@@ -56,7 +58,7 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
| --- | --- | --- | --- |
| 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 and under which the new project is created by default | path | [UC-002] step 2 "working folder" |
| 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
@@ -66,6 +68,7 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
| 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
@@ -83,3 +86,4 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
[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
+10 -6
View File
@@ -10,10 +10,11 @@
| 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]. The operation `provideProjectDetails` is the one of [OC-001]; the only change is the base of its default `directory`, stated in P4.
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
@@ -21,7 +22,7 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-003]. `
| --- | --- |
| Operation | `startFromWorkingFolder(configPath: Path [0..1], envPath: Path [0..1]): PromptSet` |
| Traces to | `startFromWorkingFolder` in [SSD-002] |
| Concepts | Run, CommandLink, Checkout, WorkingFolder, Configuration |
| Concepts | Run, CommandLink, Checkout, WorkingFolder, ConfigFiles, Configuration |
**Preconditions**
@@ -34,16 +35,18 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-003]. `
- 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 `Configuration` instance was created and associated with the `Run` from `configPath`, or from `config.env` in the `Checkout` when `configPath` is absent, and from `envPath`, or from `.env` in the `Checkout` when `envPath` is absent; the validation of [OC-001] `startProjectCreation` P2 and P3 applies.
- P6. The `Run` was associated with a `PromptSet` that is returned.
- 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 found in the `Checkout` and no path was given (P5) | The `Run` ends with an error naming the path it looked in and the options `--config` and `--env`; nothing was changed |
| A value in `config.env` or `.env` is malformed (P5) | As in [OC-001] `startProjectCreation`: the error names the key, never its value; 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 |
---
@@ -52,3 +55,4 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-003]. `
[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
+22 -11
View File
@@ -10,6 +10,7 @@
| 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] |
---
@@ -28,6 +29,7 @@ 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
@@ -37,13 +39,20 @@ L -> CK : Checkout(path)
L -> L : currentFolder()
create WF
L -> WF : WorkingFolder(path)
alt the Checkout's own files are not found
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(checkout, workingFolder, configPath, envPath)
L -> PC : startProjectCreation(workingFolder, configFiles)
activate PC
alt config.env or .env not found, or a value malformed
PC --> L : error naming the path or the key
alt a value in config.env or .env is malformed
PC --> L : error naming the key
end
PC --> L : promptSet
deactivate PC
@@ -56,9 +65,9 @@ deactivate L
| Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- |
| Information Expert | `Launcher.resolveCheckout` | The launcher knows how the script was invoked, so it is the one that can follow the command link |
| 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 `checkout` and `workingFolder` as values | The use case [UC-001] runs unchanged whatever way the script was started |
| Low Coupling | `ProjectCreator` receives `workingFolder` and `configFiles` as values | The use case [UC-001] runs unchanged whatever way the script was started |
### Postcondition Coverage
@@ -67,14 +76,15 @@ deactivate L
| 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(checkout, workingFolder, ...)`; the prompt default is built from `workingFolder` |
| P5 Configuration | `startProjectCreation` loads `config.env` and `.env` from `checkout` or from the given paths (`load` of [SD-001]) |
| P6 PromptSet | the returned `promptSet` |
| Exceptions: files not found; configuration not found or malformed | the two `alt` fragments |
| 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 and the working folder; it reads no configuration and makes no repository. `ProjectCreator` keeps every other responsibility of [SD-001].
`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].
---
@@ -83,3 +93,4 @@ deactivate L
[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
+4 -1
View File
@@ -10,6 +10,7 @@
| 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] |
---
@@ -37,7 +38,7 @@ S --> A : creation summary with the project's full path
| 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. `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.
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
@@ -48,4 +49,6 @@ One script run, as in [UC-001]. The command link persists between runs; the syst
[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
+10 -5
View File
@@ -10,6 +10,7 @@
| 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] |
---
@@ -25,7 +26,7 @@
- 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 and holds `config.env` and `.env` as described in the README, or the Maintainer points to them with `--config` and `--env`.
- 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.
@@ -37,7 +38,7 @@
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 reads `config.env` and `.env` from the checkout, or from the files named by `--config` and `--env`.
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.
@@ -49,8 +50,10 @@
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 found:
1. The system stops before any change, names the path it looked in and says that `--config` and `--env` can point elsewhere.
- 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
@@ -58,7 +61,8 @@
| --- | --- |
| 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 read from the checkout, never from the working folder, unless `--config` and `--env` name them |
| 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 |
@@ -73,3 +77,4 @@
[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
+6 -4
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | US-001.06: AGPL-3.0 default only for a public project with GitHub; US-001.07: the framework's own submodules (qc) are fetched | [1cd27f7] |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Added US-002 (global command from the target folder) for UC-002 | [1cd27f7] |
| 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] |
---
@@ -136,10 +136,11 @@ One further epic, "Start the script as a global command" ([UC-002]), with one st
**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, finds its own files and reads `config.env` and `.env` from the checkout.
- 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 other files, then those are read instead of the checkout's.
- Given a missing file or a broken link, then the script stops before any change and names the folder or path it looked in.
- 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 |
@@ -165,3 +166,4 @@ Valuable, Negotiable, Estimable, Small and Testable hold for each story. Indepen
[MIL-007]: ./milestones/mil-007-framework-checklists.md
[PP-001]: ./project-plan.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+36 -13
View File
@@ -10,10 +10,11 @@
# repositories and the mirror, then the local project: its directory, git
# repository, remotes (no credential in any address), the framework as a
# 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
# license to the Gitea repository. 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.
# chosen) and its templates. The license of the Gitea repository is
# 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
# Without --apply the script only reads from GitHub and Gitea (GET
@@ -89,18 +90,39 @@ if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4)));
fi
# Where this script lives; the library files and the project root are found
# from here, never from the current directory.
readonly SCRIPT_FILE="${BASH_SOURCE[0]}"
case "${BASH_SOURCE[0]}" in
*/*) script_path_dir="${BASH_SOURCE[0]%/*}" ;;
# from here, never from the current directory. A link to the script (a
# command in a folder on PATH) is followed to the real file, however many
# links lie on the way; readlink without -f exists on Linux, macOS and Git
# 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="." ;;
esac
SCRIPT_DIR="$(cd "$script_path_dir" && pwd)"
SCRIPT_DIR="$(cd -P "$script_path_dir" && pwd -P)"
readonly SCRIPT_DIR
unset script_path_dir
# The script lives in src/; the configuration files live one level up, in
# the project root, next to config.env.example and .env.example.
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
unset script_path script_path_dir script_link_count script_link_target
# The script lives in src/; the checkout is one level up, next to
# config.env.example and .env.example.
PROJECT_ROOT="$(cd -P "$SCRIPT_DIR/.." && pwd -P)"
readonly PROJECT_ROOT
# shellcheck source=lib/constants.sh
@@ -170,6 +192,7 @@ main() {
trap finish EXIT
is_valid_repo_name "$PROJECT_NAME" ||
die "REPOFOUNDRY_NAME is not a valid project name"
WORKING_FOLDER="$(pwd -P)"
parse_args "$@"
check_tools
setup_temp_dir
+2 -2
View File
@@ -48,9 +48,9 @@ confirm_reuse() {
die "stopped: choose another name or remove the existing repository"
fi
done
if ((${STATE[reuse_gitea]:-0})) && ((PROJECT[has_github])) &&
if ((${STATE[reuse_gitea]:-0})) && [[ -n ${PROJECT[license]} ]] &&
[[ ${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
}
+2
View File
@@ -9,6 +9,8 @@
usage() {
cat <<EOF
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
EOF
}
+54 -1
View File
@@ -142,6 +142,7 @@ validate_project_presets() {
check_preset GITHUB_OWNER is_valid_github_owner "$HINT_GITHUB_OWNER"
check_preset PROJECT_DIRECTORY is_valid_directory "$HINT_DIRECTORY"
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
@@ -190,11 +191,63 @@ warn_if_env_unsafe() {
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() {
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
validate_config
confirm_folder_files
# .env is optional: a credential it does not provide is asked.
if [[ -e $ENV_FILE ]]; then
if [[ -n $ENV_FILE && -e $ENV_FILE ]]; then
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
warn_if_env_unsafe "$ENV_FILE"
fi
+11 -3
View File
@@ -18,6 +18,7 @@ readonly DEFAULT_MIRROR_INTERVAL="10m0s"
readonly DEFAULT_SSH_PORT=10022
readonly DEFAULT_FRAMEWORK_REPO="TirSystem/SQA-QC-Framework"
readonly AGPL_LICENSE_KEY="AGPL-3.0"
readonly NO_LICENSE_WORD="none"
readonly DEFAULT_BRANCH="main"
# What the prompts and the preset keys in config.env both tell the Maintainer
# when a value is refused.
@@ -25,6 +26,7 @@ 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_GITEA_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_TOKEN="8 to 255 letters, digits or _ . ~ + / = -"
readonly ENV_FILE_NAME=".env"
@@ -34,11 +36,17 @@ readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
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)
CONFIG_FILE="$PROJECT_ROOT/config.env"
ENV_FILE="$PROJECT_ROOT/.env"
# The configuration files: named by --config and --env, or chosen by
# 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=""
HAS_JQ=0
HTTP_STATUS=0
+15 -1
View File
@@ -4,7 +4,7 @@
#
# 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
# SSH to Gitea is not available (the Maintainer agreed to that).
@@ -12,6 +12,18 @@ is_framework_skipped() {
[[ ${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
# needed for it; a failure says how to test the access.
add_framework() {
@@ -27,6 +39,7 @@ add_framework() {
if [[ $existing != "$url" ]]; then
die "'framework' already exists in $dir and is not the framework submodule ($url)"
fi
init_framework_submodules "$dir"
finish_step "$label" "reused" "$url (already a submodule)"
return 0
fi
@@ -35,6 +48,7 @@ add_framework() {
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' ' ')"
fi
init_framework_submodules "$dir"
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
# 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() {
local label="Local project" dir="${PROJECT[directory]}"
begin_step "$label"
@@ -84,6 +84,8 @@ create_local_project() {
ensure_remote "$dir" origin "$(origin_url)"
if ((PROJECT[has_github])); then
ensure_remote "$dir" github "$(github_remote_url)"
fi
if [[ -n ${PROJECT[license]} ]]; then
checkout_gitea_history "$dir"
fi
finish_step "$label" "created" "$dir (origin over $(origin_protocol))"
+2 -2
View File
@@ -22,8 +22,8 @@ print_plan() {
say "Plan:"
if ((STATE[reuse_gitea])); then
gitea_action="reuse the existing repository (you will be asked to confirm)"
elif ((PROJECT[has_github])); then
gitea_action="create (${PROJECT[visibility]}) with the $AGPL_LICENSE_KEY license"
elif [[ -n ${PROJECT[license]} ]]; then
gitea_action="create (${PROJECT[visibility]}) with the ${PROJECT[license]} license$(license_note)"
else
gitea_action="create (${PROJECT[visibility]}), empty"
fi
+4 -4
View File
@@ -23,8 +23,8 @@ check_gitea_organization() {
check_gitea_license() {
api_call gitea GET /licenses
expect_status "cannot list the licenses of the Gitea server" 200
json_has_value "$HTTP_BODY_FILE" key "$AGPL_LICENSE_KEY" ||
die "the Gitea server does not offer the $AGPL_LICENSE_KEY license"
json_has_value "$HTTP_BODY_FILE" key "${PROJECT[license]}" ||
die "the Gitea server does not offer the ${PROJECT[license]} license"
}
# inspect_repository HOST: record in STATE[HOST_repo] whether the repository
@@ -70,7 +70,7 @@ preflight_gitea() {
check_gitea_organization "$owner" "$login"
STATE[gitea_owner_kind]="organization"
fi
if ((PROJECT[has_github])); then
if [[ -n ${PROJECT[license]} ]]; then
check_gitea_license
fi
inspect_repository gitea
@@ -143,7 +143,7 @@ decide_existing_repositories() {
free) ;;
empty) STATE[reuse_$host]=1 ;;
initial_only)
if [[ $host == gitea ]] && ((PROJECT[has_github])); then
if [[ $host == gitea && -n ${PROJECT[license]} ]]; then
STATE[reuse_$host]=1
else
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.
#
# 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;
# the value is returned in REPLY and marked in PRESET[NAME].
@@ -14,6 +14,33 @@ preset_detail() {
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.
collect_project_details() {
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"
PROJECT[gitea_owner]="$REPLY"
collect_github_details
resolve_license
preset_detail PROJECT_DIRECTORY directory ||
prompt_value "Local directory" "./${PROJECT[name]}" is_valid_directory "$HINT_DIRECTORY"
PROJECT[directory]="$REPLY"
@@ -46,7 +74,7 @@ collect_github_details() {
if preset_detail USE_GITHUB has_github; then
[[ $REPLY == yes ]] && REPLY=1 || REPLY=0
else
prompt_yes_no "Also create a GitHub repository (applies the AGPL license)" y
prompt_yes_no "Also create a GitHub repository" y
fi
PROJECT[has_github]="$REPLY"
PROJECT[github_owner]=""
@@ -90,10 +118,11 @@ print_summary() {
say " Description : ${PROJECT[description]:-(none)}$(source_note description)"
say " Gitea : ${CONFIG[GITEA_URL]}/${PROJECT[gitea_owner]}/${PROJECT[name]}$(source_note gitea_owner)"
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
say " GitHub : not used$(source_note has_github)"
fi
say " License : ${PROJECT[license]:-none}$(license_note)"
say " Directory : ${PROJECT[directory]}$(source_note directory)"
say " Plan gate : $(yes_no "${PROJECT[is_plan_gate_enabled]}")$(source_note is_plan_gate_enabled)"
say "Credentials : GITEA_TOKEN $(credential_state GITEA_TOKEN)," \
+2 -2
View File
@@ -18,8 +18,8 @@ repo_body() {
"${PROJECT[name]}" "$description" "$private"
return 0
fi
if ((PROJECT[has_github])); then
extra=',"auto_init":true,"license":"'"$AGPL_LICENSE_KEY"'"'
if [[ -n ${PROJECT[license]} ]]; then
extra=',"auto_init":true,"license":"'"${PROJECT[license]}"'"'
else
extra=',"auto_init":false'
fi
+5
View File
@@ -58,6 +58,11 @@ is_valid_port() {
[[ $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.
is_valid_interval() {
[[ -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
# yes or no, GitHub owner, directory, plan gate.
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'
SHARED_REMOTES=""
@@ -110,12 +120,15 @@ write_gitconfig() {
insteadOf = https://git.example.test/
[url "file://$WORK/remote/"]
insteadOf = ssh://git@git.example.test:10022/
[url "file://$WORK/remote/"]
insteadOf = ssh://git@git.tirsystem.com:10022/
EOF
}
# 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
# 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() {
if [[ -n $SHARED_REMOTES && -d $SHARED_REMOTES ]]; then
return 0
@@ -123,6 +136,7 @@ ensure_shared_remotes() {
SHARED_REMOTES="$(mktemp -d "${TMPDIR:-/tmp}/repofoundry-remotes.XXXXXX")"
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/qc" "$SHARED_REMOTES/TirSystem/SQA-QC-Checklists.git"
git init -q --bare "$SHARED_REMOTES/TirSystem/my-app.git"
git init -q "$SHARED_REMOTES/seed"
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.
remove_workdir() {
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 {} +
fi
WORK=""
+23 -9
View File
@@ -31,7 +31,7 @@ test_dry_run_prints_the_plan_and_only_reads() {
setup_hosts
run_dry "$ANSWERS_GITHUB"
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 "mirror plan" "$OUT" "Push mirror : Gitea -> GitHub every 10m0s"
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"
}
test_license_must_be_offered_when_github_is_chosen() {
test_license_must_be_offered_when_a_public_project_has_github() {
setup_hosts
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_contains "message" "$ERR" "does not offer the AGPL-3.0 license"
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
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
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")"
assert_contains "GitHub name" "$bodies" '"name":"my-app"'
assert_contains "GitHub is created empty" "$bodies" '"private":true,"auto_init":false}'
assert_contains "Gitea gets the license" "$bodies" '"license":"AGPL-3.0"'
assert_contains "Gitea is initialised with it" "$bodies" '"auto_init":true'
assert_not_contains "a private project gets no license" "$bodies" '"license"'
assert_contains "default branch" "$bodies" '"default_branch":"main"'
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"'
@@ -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"
}
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() {
setup_hosts
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
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"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n'
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "license only" 0 "$STATUS"
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")"
@@ -355,7 +369,7 @@ test_gitea_repository_with_only_the_license_can_be_reused() {
test_reusing_an_empty_gitea_repository_warns_about_the_license() {
setup_hosts
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_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
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"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n'
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "LICENSE and README.md" 0 "$STATUS"
assert_not_contains "not created again" "$(calls)" "$GITEA_REPO_CALL"
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() {
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
printf -v LOCAL_ANSWERS 'my-app\n\n\nTirSystem\nn\n%s\n%s\n' "$1" "$3"
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 "description" "$OUT" "Description : A test 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 "plan gate" "$OUT" "Plan gate : no (from config.env)"
}
+8 -6
View File
@@ -19,10 +19,10 @@ test_full_run_with_github() {
after="$(listing)"
assert_status "full run" 0 "$STATUS"
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 "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_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"
@@ -201,13 +201,15 @@ test_default_files_are_in_the_project_root() {
cp "$WORK/config.env" "$WORK/project/config.env"
cp "$WORK/.env" "$WORK/project/.env"
STATUS=0
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=$?
(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=$?
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"
# 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
(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=$?
assert_status "run from another directory" 0 "$STATUS"
}