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

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

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

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

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

Task: MIL-006#1
Task: MIL-006#2
Task: MIL-006#3
Task: MIL-006#4
Refs #44
Refs #45
Refs #46
Refs #50
2026-10-07 13:08:49 +08:00
Tirsvad b324acbfae Resolve pending commit links for the default configuration files 2026-10-07 12:42:08 +08:00
Tirsvad 0ab50068bf Plan the default configuration files: working folder first, then the checkout
- --config and --env, else ./config.env and ./.env, else the checkout's
- Files named before any request; a file from the working folder needs a yes
- UC-002, US-002, MIL-007 criteria 9 and 10, models and contracts updated
- Review record RC-029
2026-10-07 12:42:05 +08:00
Tirsvad 88b4e5ce6c Resolve pending commit links for the task renames 2026-10-07 12:38:02 +08:00
Tirsvad be759e3832 Give the test tasks of MIL-006 and MIL-007 their own titles
The sync matches issues by title, so Test every case moved issue #39 of MIL-005.
2026-10-07 12:38:01 +08:00
43 changed files with 947 additions and 675 deletions
+2 -5
View File
@@ -1,9 +1,6 @@
# RepoFoundry credentials. Placeholders only: never put a real value in this # RepoFoundry credentials. Placeholders only: never put a real value in this
# file or commit one. # file or commit one.
# #
# Everything in this file is optional: a credential that is missing here is
# asked for when the script runs (the token is not echoed).
#
# Copy this file to .env, fill in the values and keep it private # Copy this file to .env, fill in the values and keep it private
# (chmod 600 .env on Linux and macOS). .env is ignored by git. The file is # (chmod 600 .env on Linux and macOS). .env is ignored by git. The file is
# read as plain KEY=VALUE lines and never executed. Values may be wrapped in # read as plain KEY=VALUE lines and never executed. Values may be wrapped in
@@ -16,7 +13,7 @@
GITHUB_PAT= GITHUB_PAT=
######################################## ########################################
# Secrets for framework # Secrets for workframe
######################################## ########################################
# GitHub account the token belongs to. It identifies who authenticates; it is # GitHub account the token belongs to. It identifies who authenticates; it is
@@ -24,6 +21,6 @@ GITHUB_PAT=
# belong to an organization. # belong to an organization.
GITHUB_USER= GITHUB_USER=
# Gitea access token. It needs permission to create repositories # Gitea access token (required). It needs permission to create repositories
# for the chosen owner and to manage the repository's push mirror. # for the chosen owner and to manage the repository's push mirror.
GITEA_TOKEN= GITEA_TOKEN=
+86 -44
View File
@@ -59,13 +59,73 @@ src/create-project.sh --help
Nothing has to be installed system-wide: the script runs from the checkout and Nothing has to be installed system-wide: the script runs from the checkout and
loads its own files from `src/lib/`. loads its own files from `src/lib/`.
### Run it from the folder where the project is to be created
The new project is created under the folder you start the script in: the
default directory is `./<repository name>`. Go to the folder that should hold
the project, then start the script from there, by its path or by a global
command (below):
```bash
cd ~/work # the folder that will hold my-app
~/src/RepoFoundry/src/create-project.sh # creates ~/work/my-app
```
### Make it a global command
Put a link to the script in a folder that is on your `PATH`. The script
follows the link to the checkout, so it still finds its own files there.
Linux, macOS and Git Bash on Windows (run this once, from the checkout):
```bash
mkdir -p ~/bin
ln -s "$PWD/src/create-project.sh" ~/bin/repo-foundry
```
If `~/bin` is not on your `PATH` yet, add it and open a new shell:
```bash
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc # ~/.zshrc on macOS
```
Check that it works, from any folder:
```bash
cd ~/work
repo-foundry --version # prints the name and version
repo-foundry # a dry run that creates nothing
```
On Windows, Git Bash makes a *copy* instead of a link unless symbolic links
are allowed (Developer Mode, or an administrator shell). Either allow them and
run `export MSYS=winsymlinks:nativestrict` before the `ln -s`, or use an alias
in `~/.bashrc`, which works because the script finds its own folder:
```bash
alias repo-foundry='bash /c/Users/me/RepoFoundry/src/create-project.sh'
```
## Configuration ## Configuration
The script reads two plain files from the project root. They are **parsed, The script reads two plain files. They are **parsed,
never executed** (`source` is not used): only `KEY=VALUE` lines with known keys never executed** (`source` is not used): only `KEY=VALUE` lines with known keys
are accepted, and anything else stops the run with a message that names the key are accepted, and anything else stops the run with a message that names the key
and the line, never the value. and the line, never the value.
Each file is chosen on its own, in this order:
1. the file named with `--config` or `--env`;
2. `./config.env` or `./.env` in the folder you start the script in;
3. `config.env` or `.env` in the checkout (next to `src/`).
The script names the files it uses before it contacts any host. A file taken
from the folder you started in is also confirmed: the script shows the file
names and the Gitea address and asks for a yes (default no) before the first
request, because a `config.env` in a folder you do not control could point
Gitea at another host and so send your token there. A file you name with
`--config` or `--env`, or the checkout's own, is not asked about.
```bash ```bash
cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL) cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL)
cp .env.example .env # credentials: keep private cp .env.example .env # credentials: keep private
@@ -103,6 +163,7 @@ A detail that is set is used and not asked; the summary marks it with
| `GITHUB_OWNER` | GitHub user or organization | letters, digits, `-`; used only when GitHub is used | | `GITHUB_OWNER` | GitHub user or organization | letters, digits, `-`; used only when GitHub is used |
| `PROJECT_DIRECTORY` | local directory | not empty, not starting with `-` | | `PROJECT_DIRECTORY` | local directory | not empty, not starting with `-` |
| `ENABLE_PLAN_GATE` | enable the plan gate | `yes` or `no` | | `ENABLE_PLAN_GATE` | enable the plan gate | `yes` or `no` |
| `PROJECT_LICENSE` | license of the project | a Gitea license key (letters, digits, `.`, `+`, `-`; at most 64), such as `AGPL-3.0` or `MIT`, or `none` |
- A key that is present counts as set, even when its value is empty. Only - A key that is present counts as set, even when its value is empty. Only
`PROJECT_DESCRIPTION` may be empty (no description); an empty value for any `PROJECT_DESCRIPTION` may be empty (no description); an empty value for any
@@ -111,7 +172,12 @@ A detail that is set is used and not asked; the summary marks it with
key. The script never falls back to asking for it. key. The script never falls back to asking for it.
- `USE_GITHUB=no` skips the GitHub owner and every GitHub step; a - `USE_GITHUB=no` skips the GitHub owner and every GitHub step; a
`GITHUB_OWNER` set at the same time is ignored, with a warning. `GITHUB_OWNER` set at the same time is ignored, with a warning.
- Only these eight details can be set. The confirmations stay questions that - `PROJECT_LICENSE` is never asked. When set, that license is put on the Gitea
repository with or without GitHub, and `none` means no license. When absent,
AGPL-3.0 is applied only if GitHub is used **and** the project is public;
a private project, or one without GitHub, gets no license. The Gitea server
must offer the license, or the run stops before anything is created.
- Only these nine details can be set. The confirmations stay questions that
default to no: create now, reusing an existing repository, an existing default to no: create now, reusing an existing repository, an existing
directory, `core.hooksPath` and replacing a template file. directory, `core.hooksPath` and replacing a template file.
- These keys are accepted in `config.env` only, never in `.env`. - These keys are accepted in `config.env` only, never in `.env`.
@@ -119,7 +185,7 @@ A detail that is set is used and not asked; the summary marks it with
value there. Put a description that contains ` #` in double quotes, for value there. Put a description that contains ` #` in double quotes, for
example `PROJECT_DESCRIPTION="Tool for #mirrors"`. example `PROJECT_DESCRIPTION="Tool for #mirrors"`.
With all eight set, a run asks only the confirmations: With all of them set, a run asks only the confirmations:
```bash ```bash
src/create-project.sh --apply # asks only "Create these now (y/n) [n]" src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
@@ -129,40 +195,14 @@ src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
| Key | Meaning | | Key | Meaning |
| --- | --- | | --- | --- |
| `GITEA_TOKEN` | Gitea access token | | `GITEA_TOKEN` | Gitea access token (required) |
| `GITHUB_PAT` | GitHub personal access token (only when you choose GitHub) | | `GITHUB_PAT` | GitHub personal access token (only when you choose GitHub) |
| `GITHUB_USER` | the GitHub account the token belongs to (only when you choose GitHub) | | `GITHUB_USER` | the GitHub account the token belongs to; only a default for the owner prompt |
`.env` is ignored by git. The script warns if it is readable by other users or `.env` is ignored by git. The script warns if it is readable by other users or
not ignored by git. See [Token permissions](#token-permissions) for what each not ignored by git. See [Token permissions](#token-permissions) for what each
token needs. token needs.
`.env` is optional, and so is each key in it. A credential that is not
provided (the file is missing, the key is absent or its value is empty) is
asked for: the Gitea token at the start, the GitHub token and account name once
you choose GitHub. A token is read without echo and checked like one read from
`.env`; a refused value is asked again and never shown. If input ends before a
valid value is entered, the run stops before any request to a host.
### The project's own `.env`
When the project exists, the script asks whether to create a `.env` in it
(default no). On a yes the file holds only the credentials the project needs:
`GITEA_TOKEN`, and `GITHUB_PAT` and `GITHUB_USER` when you chose GitHub, as read
from your `.env` or typed.
- The file is created readable by you only (mode 600), never readable by
others even for a moment, and is never written by anything else.
- Git ignores it: the script adds `.env` to `.git/info/exclude` of the new
project. No tracked file changes and nothing is committed.
- An existing `.env` in the project is never replaced without a second yes, and
a `.env` that git already tracks is never written.
- The summary names the keys, never the values.
This is the one place the script writes a token to disk. It is plain text: keep
the project directory private, do not copy the file around, and say no if you
do not need it. Tokens are written nowhere else.
## Usage ## Usage
```bash ```bash
@@ -171,6 +211,11 @@ src/create-project.sh --apply # creates everything after a final yes
src/create-project.sh --config /path/to/config.env --env /path/to/.env src/create-project.sh --config /path/to/config.env --env /path/to/.env
``` ```
With a global command (see [Installation](#installation)) the same commands are
`repo-foundry`, `repo-foundry --apply` and so on, started from the folder that
should hold the project. Without `--config` and `--env` the files are looked
for as described under [Configuration](#configuration).
The script asks for, in this order: repository name, description, visibility, The script asks for, in this order: repository name, description, visibility,
Gitea owner, whether to also create a GitHub repository (and its owner), the Gitea owner, whether to also create a GitHub repository (and its owner), the
local directory and whether to enable the plan gate (a detail set in local directory and whether to enable the plan gate (a detail set in
@@ -179,8 +224,8 @@ with read-only requests and prints a plan:
```text ```text
Plan: Plan:
Gitea repository : create (private) with the AGPL-3.0 license https://git.example.org/Team/my-app Gitea repository : create (public) with the AGPL-3.0 license (default: GitHub and a public project) https://git.example.org/Team/my-app
GitHub repository : create (private), empty https://github.com/acme/my-app GitHub repository : create (public), empty https://github.com/acme/my-app
Push mirror : Gitea -> GitHub every 10m0s Push mirror : Gitea -> GitHub every 10m0s
Local project : create ./my-app (new directory), git on main, no commit Local project : create ./my-app (new directory), git on main, no commit
Local origin : will use SSH (the SSH test passed) Local origin : will use SSH (the SSH test passed)
@@ -193,7 +238,7 @@ Without `--apply` that is all that happens. With `--apply` the script asks
"Create these now" (default no) and then creates, in this order: "Create these now" (default no) and then creates, in this order:
1. the GitHub repository (empty), if chosen; 1. the GitHub repository (empty), if chosen;
2. the Gitea repository (with the AGPL-3.0 license if GitHub was chosen); 2. the Gitea repository (with the license that applies: `PROJECT_LICENSE`, or AGPL-3.0 for a public project with GitHub);
3. the push mirror Gitea -> GitHub, and a request for its first sync; 3. the push mirror Gitea -> GitHub, and a request for its first sync;
4. the local directory, `git init` on `main`, the `origin` remote (and `github` 4. the local directory, `git init` on `main`, the `origin` remote (and `github`
if chosen), and, if the Gitea repository holds the license commit, that if chosen), and, if the Gitea repository holds the license commit, that
@@ -207,10 +252,11 @@ hooks refuse commits on `main`.
### Choices ### Choices
- **GitHub or not.** Choosing GitHub also applies the AGPL-3.0 license to the - **GitHub or not.** Choosing GitHub sets up the mirror. For a public project it
Gitea repository (so it is not empty) and sets up the mirror. Without GitHub also applies the AGPL-3.0 license to the Gitea repository (so it is not
the Gitea repository is empty and has no license, and `GITHUB_PAT` is not empty), unless `PROJECT_LICENSE` says otherwise. A private project with
needed. GitHub gets no license by default. Without GitHub the Gitea repository has
no license unless `PROJECT_LICENSE` sets one, and `GITHUB_PAT` is not needed.
- **Owners.** The Gitea owner and the GitHub owner are chosen separately and - **Owners.** The Gitea owner and the GitHub owner are chosen separately and
may be a user or an organization. `GITHUB_USER` is only the suggested default may be a user or an organization. `GITHUB_USER` is only the suggested default
for the GitHub owner prompt; it identifies who authenticates. for the GitHub owner prompt; it identifies who authenticates.
@@ -290,9 +336,7 @@ script stops before it creates anything when a preflight check is refused.
## Security decisions ## Security decisions
- **Tokens never appear** in output, logs, remote URLs, `.git/config`, - **Tokens never appear** in output, logs, remote URLs, `.git/config`,
`.gitmodules`, command lines or leftover files, and are written to disk only `.gitmodules`, command lines or leftover files. They go to `curl` through a
in the new project's own `.env`, after a yes (see
[The project's own `.env`](#the-projects-own-env)). They go to `curl` through a
private configuration file that is removed right after the request, and to private configuration file that is removed right after the request, and to
`git` (HTTPS fetch only) through a `GIT_ASKPASS` helper and the environment `git` (HTTPS fetch only) through a `GIT_ASKPASS` helper and the environment
of that one command. Output is filtered, so even a server message that echoes of that one command. Output is filtered, so even a server message that echoes
@@ -396,13 +440,11 @@ file. The files are loaded from that directory only, by a fixed path.
| `json.sh` | the little JSON the script reads and writes | | `json.sh` | the little JSON the script reads and writes |
| `http.sh` | the one place that runs `curl`; tokens stay off the command line | | `http.sh` | the one place that runs `curl`; tokens stay off the command line |
| `api.sh` | GitHub and Gitea API calls and reporting a refused call | | `api.sh` | GitHub and Gitea API calls and reporting a refused call |
| `prompts.sh` | interactive questions with validation (secrets are read without echo) | | `prompts.sh` | interactive questions with validation |
| `credentials.sh` | asking for a credential that `.env` does not provide |
| `project.sh` | the project details: asking for them and showing them | | `project.sh` | the project details: asking for them and showing them |
| `hosts.sh` | names, links and remote addresses of the repositories | | `hosts.sh` | names, links and remote addresses of the repositories |
| `preflight.sh` | read-only checks of both hosts | | `preflight.sh` | read-only checks of both hosts |
| `steps.sh` | the outcome of each step and the final report | | `steps.sh` | the outcome of each step and the final report |
| `envfile.sh` | the new project's own `.env`: created private, ignored by git, never replaced without a yes |
| `plan.sh` | printing what the script is about to do | | `plan.sh` | printing what the script is about to do |
| `repositories.sh` | creating the GitHub and Gitea repositories | | `repositories.sh` | creating the GitHub and Gitea repositories |
| `mirror.sh` | the Gitea to GitHub push mirror | | `mirror.sh` | the Gitea to GitHub push mirror |
+6
View File
@@ -46,6 +46,12 @@ GITEA_API_URL=https://<your gitea instance>/api/v1
#PROJECT_DIRECTORY=./my-project #PROJECT_DIRECTORY=./my-project
#ENABLE_PLAN_GATE=no # yes or no #ENABLE_PLAN_GATE=no # yes or no
# Optional. The license of the project, as a Gitea license key (AGPL-3.0, MIT,
# ...), or none for no license. It is never asked. When absent, AGPL-3.0 is
# applied only if GitHub is used and the project is public; otherwise there
# is no license. The Gitea server must offer the license.
#PROJECT_LICENSE=AGPL-3.0
# Optional. OWNER/NAME of the SQA-QC-Framework repository on the Gitea server; # Optional. OWNER/NAME of the SQA-QC-Framework repository on the Gitea server;
# it is added to the new project as a submodule over SSH. # it is added to the new project as a submodule over SSH.
# Default: TirSystem/SQA-QC-Framework. # Default: TirSystem/SQA-QC-Framework.
+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 | | DCD | Design Class Diagram | docs/dcd.md | 004 |
| DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 | | DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 029 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 030 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
## Languages ## Languages
+5 -4
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | 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 | 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. 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. 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. 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 ## 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). - 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. - A project license set in `config.env` (`PROJECT_LICENSE`, optional, never asked), checked against the licenses the Gitea server offers.
- Partial-failure reporting with a documented way to continue. - Partial-failure reporting with a documented way to continue.
- Starting through a command link: the script finds its own files from the link, 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. - Fetching the framework's own submodules (`git submodule update --init --recursive`), so the `qc` checklists are present.
- Documentation of the SSH prerequisite for the submodule (Gitea SSH on port `10022`). - Documentation of the SSH prerequisite for the submodule (Gitea SSH on port `10022`).
@@ -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 | | 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 | | 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 | | 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 ## Risks
@@ -154,3 +154,4 @@ Proceed — the procedure is small, well bounded and removes a repeated, securit
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[UCD-001]: ./use-case-diagram.md [UCD-001]: ./use-case-diagram.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | 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 | 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>> { 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 +provideProjectDetails(name : String, description : String, visibility : Visibility, giteaOwner : Owner, githubOwner : Owner [0..1], directory : Path, enablePlanGate : Boolean, writeEnvFile : Boolean) : Summary
} }
class ConfigLoader { class ConfigLoader {
@@ -83,6 +83,7 @@ class SummaryReport {
class Launcher { class Launcher {
+resolveCheckout(invocation : Path) : Checkout +resolveCheckout(invocation : Path) : Checkout
+currentFolder() : WorkingFolder +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 +startFromWorkingFolder(configPath : Path [0..1], envPath : Path [0..1]) : PromptSet
} }
class Checkout { class Checkout {
@@ -92,6 +93,12 @@ class Checkout {
} }
class WorkingFolder { class WorkingFolder {
-path : Path -path : Path
+configFile() : Path [0..1]
+envFile() : Path [0..1]
}
class ConfigFiles {
-configFile : Path
-envFile : Path
} }
class Run { class Run {
-isApply : Boolean -isApply : Boolean
@@ -186,6 +193,8 @@ class Summary {
Launcher "1" --> "1" ProjectCreator : starts Launcher "1" --> "1" ProjectCreator : starts
Launcher ..> Checkout : creates Launcher ..> Checkout : creates
Launcher ..> WorkingFolder : creates Launcher ..> WorkingFolder : creates
Launcher ..> ConfigFiles : creates
Run "1" *-- "1" ConfigFiles
Run "1" *-- "1" Checkout Run "1" *-- "1" Checkout
Run "1" *-- "1" WorkingFolder Run "1" *-- "1" WorkingFolder
ProjectCreator ..> ConfigLoader : creates ProjectCreator ..> ConfigLoader : creates
@@ -249,9 +258,10 @@ Repository "0..*" --> "1" Visibility
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations | | 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` | | `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` | | `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` | | `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` | | `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 | | 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.startFromWorkingFolder(configPath, envPath) : PromptSet` | [OC-002] `startFromWorkingFolder`; [SD-002] |
| `Launcher.resolveCheckout(invocation) : Checkout`, `Launcher.currentFolder() : WorkingFolder` | [OC-002] P2, P3; [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(...)` | | `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 | | `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` | | `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 [SD-001]: ./uc-001/sd.md
[MIL-005]: ./milestones/mil-005-credentials.md [MIL-005]: ./milestones/mil-005-credentials.md
[DICT-001]: ./dictionary.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 [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 ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | 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 | 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 - The Domain Model, use cases and user stories use the PO term; the Operation
Contract, Sequence Diagram, Design Class Diagram and ERD use the IT term. Contract, Sequence Diagram, Design Class Diagram and ERD use the IT term.
- One IT term per PO term and one PO term per IT term; no synonyms. - One IT term per PO term and one PO term per IT term; no synonyms.
- `Run`, `ToolCheck`, `PreflightResult` and `PromptSet` appear in [OC-001] but - `Run`, `ToolCheck`, `PreflightResult`, `PromptSet` and `ConfigFiles` (the two
have no PO term: they are system concepts, not domain concepts, and are not files chosen for the Configuration, in [OC-002]) appear in the Operation
Contracts but have no PO term: they are system concepts, not domain concepts, and are not
in the Domain Model. in the Domain Model.
- `InstallResult` and the enumeration `Visibility` appear only in [DCD-001]: - `InstallResult` and the enumeration `Visibility` appear only in [DCD-001]:
`InstallResult` carries the three results of one operation, and `Visibility` `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 [DM-002]: ./domain-model.md
[OC-001]: ./uc-001/oc.md [OC-001]: ./uc-001/oc.md
[DCD-001]: ./uc-001/dcd.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 [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 ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | 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 | 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 CommandLink "0..*" --> "1" Checkout : leads to
Maintainer "1" --> "1" WorkingFolder : starts the script in Maintainer "1" --> "1" WorkingFolder : starts the script in
Checkout "1" --> "1" Configuration : holds by default Checkout "1" --> "1" Configuration : holds by default
WorkingFolder "1" --> "0..1" Configuration : may hold
WorkingFolder "1" --> "0..*" LocalProject : is the base of WorkingFolder "1" --> "0..*" LocalProject : is the base of
@enduml @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" | | 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" | | 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" | | 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" | | Summary | The report of what was created, skipped or failed and how to continue | created items, skipped items, next steps | [UC-001] step 10 "summary" |
## Association Table ## Association Table
@@ -189,6 +190,7 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
| Command Link | leads to | Checkout | 0..* to 1 | | Command Link | leads to | Checkout | 0..* to 1 |
| Maintainer | starts the script in | Working Folder | 1 to 1 | | Maintainer | starts the script in | Working Folder | 1 to 1 |
| Checkout | holds by default | Configuration | 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..* | | Working Folder | is the base of | Local Project | 1 to 0..* |
## Generalizations ## Generalizations
@@ -206,3 +208,4 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
[DICT-001]: ./dictionary.md [DICT-001]: ./dictionary.md
[DM-001]: ./uc-001/dm.md [DM-001]: ./uc-001/dm.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [d773fa9] | | 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 | 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 | 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] | | 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] | | 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 | | | 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 [UC-001]: ../uc-001/uc.md
[DCD-001]: ../uc-001/dcd.md [DCD-001]: ../uc-001/dcd.md
[MIL-004]: ./mil-004-configurable-details.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 [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 ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [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 | 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 | 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. 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. 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 ## 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 | | 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 | | 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 | | 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 ## 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 | | # | 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] | | 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] | | 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, `--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] | | 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 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 | | | 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-001]: ../uc-001/uc.md
[UC-002]: ../uc-002/uc.md [UC-002]: ../uc-002/uc.md
[MIL-003]: ./mil-003-scaffold-and-release.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) | | 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] | | [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] | | [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007] | [RC-012], [RC-018], [RC-020], [RC-022] |
| [MIL-001] | MIL | [BC-001], [PP-001] | [US-001] | [RC-011], [RC-016] | | [MIL-001] | MIL | [BC-001], [PP-001] | [US-001] | [RC-011], [RC-016] |
@@ -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-004] | MIL | [BC-001], [PP-001] | [US-001] | [RC-018], [RC-019] |
| [MIL-005] | MIL | [BC-001], [PP-001] | [US-001] | [RC-020] | | [MIL-005] | MIL | [BC-001], [PP-001] | [US-001] | [RC-020] |
| [MIL-006] | MIL | [BC-001], [PP-001] | [US-001] | [RC-022] | | [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] | | [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-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] | | [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] | | [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] | | [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] | | [OC-002] | OC | [SSD-002], [DM-003] | [SD-002] | [RC-026], [RC-029] |
| [SD-002] | SD | [OC-002], [DCD-003] | [DCD-003] | [RC-027] | | [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] | | [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] | | [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-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] | | [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] | | [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] | | [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] | | [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-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 ## Coverage Notes
@@ -80,6 +80,7 @@ updated whenever an artifact instance is created or reviewed.
[RC-026]: ./reviews/rc-026-oc-002.md [RC-026]: ./reviews/rc-026-oc-002.md
[RC-027]: ./reviews/rc-027-sd-002.md [RC-027]: ./reviews/rc-027-sd-002.md
[RC-028]: ./reviews/rc-028-dcd-003.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-001]: ../uc-001/dcd.md
[DCD-002]: ../dcd.md [DCD-002]: ../dcd.md
[UCD-001]: ../use-case-diagram.md [UCD-001]: ../use-case-diagram.md
+20 -6
View File
@@ -10,6 +10,7 @@
| Date | Status | Author | Reviewer | Change | Commit | | 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 | 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 { class Launcher {
+resolveCheckout(invocation : Path) : Checkout +resolveCheckout(invocation : Path) : Checkout
+currentFolder() : WorkingFolder +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 +startFromWorkingFolder(configPath : Path [0..1], envPath : Path [0..1]) : PromptSet
} }
class Checkout { class Checkout {
@@ -33,14 +35,22 @@ class Checkout {
} }
class WorkingFolder { class WorkingFolder {
-path : Path -path : Path
+configFile() : Path [0..1]
+envFile() : Path [0..1]
}
class ConfigFiles {
-configFile : Path
-envFile : Path
} }
class ProjectCreator { class ProjectCreator {
+startProjectCreation(checkout : Checkout, workingFolder : WorkingFolder, configPath : Path [0..1], envPath : Path [0..1]) : PromptSet +startProjectCreation(workingFolder : WorkingFolder, configFiles : ConfigFiles) : PromptSet
} }
class Run class Run
Launcher "1" --> "1" ProjectCreator : starts Launcher "1" --> "1" ProjectCreator : starts
Launcher ..> Checkout : creates Launcher ..> Checkout : creates
Launcher ..> WorkingFolder : creates Launcher ..> WorkingFolder : creates
Launcher ..> ConfigFiles : creates
Run "1" *-- "1" ConfigFiles
Run "1" *-- "1" Checkout Run "1" *-- "1" Checkout
Run "1" *-- "1" WorkingFolder Run "1" *-- "1" WorkingFolder
@enduml @enduml
@@ -50,9 +60,10 @@ Run "1" *-- "1" WorkingFolder
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations | | 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` | | `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. 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.startFromWorkingFolder(configPath, envPath) : PromptSet` | [SD-002] `startFromWorkingFolder`; P1, P6 |
| `Launcher.resolveCheckout(invocation) : Checkout` | [SD-002] `resolveCheckout(invocation)`; P2 | | `Launcher.resolveCheckout(invocation) : Checkout` | [SD-002] `resolveCheckout(invocation)`; P2 |
| `Launcher.currentFolder() : WorkingFolder` | [SD-002] `currentFolder()`; P3 | | `Launcher.currentFolder() : WorkingFolder` | [SD-002] `currentFolder()`; P3 |
| `Checkout.configFile() : Path`, `Checkout.envFile() : Path` | [SD-002] `startProjectCreation`; P5 | | `Launcher.locateConfigFiles(configPath, envPath, checkout, workingFolder) : ConfigFiles` | [SD-002] `locateConfigFiles(...)`; 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` | | `WorkingFolder.configFile() : Path [0..1]`, `WorkingFolder.envFile() : Path [0..1]` | [SD-002] `locateConfigFiles`; P5 |
| `Checkout.configFile() : Path`, `Checkout.envFile() : Path` | [SD-002] `locateConfigFiles`; P5 |
| `ProjectCreator.startProjectCreation(workingFolder, configFiles) : PromptSet` | [SD-002] `startProjectCreation`; P4, P6, P7. Replaces the signature of [DCD-001] by adding `workingFolder` and `configFiles` |
## Pattern Annotations ## Pattern Annotations
@@ -75,7 +88,7 @@ The concept Command Link has no class: it is a link the Maintainer makes with th
## Dependency Check ## 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-001]: ../uc-001/dcd.md
[DCD-002]: ../dcd.md [DCD-002]: ../dcd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 | | 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 | 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 CommandLink "0..*" --> "1" Checkout : leads to
Maintainer "1" --> "1" WorkingFolder : starts the script in Maintainer "1" --> "1" WorkingFolder : starts the script in
Checkout "1" --> "1" Configuration : holds by default Checkout "1" --> "1" Configuration : holds by default
WorkingFolder "1" --> "0..1" Configuration : may hold
WorkingFolder "1" --> "0..*" LocalProject : is the base of WorkingFolder "1" --> "0..*" LocalProject : is the base of
@enduml @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" | | 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" | | 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 ## Association Table
@@ -66,6 +68,7 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
| Command Link | leads to | Checkout | 0..* to 1 | | Command Link | leads to | Checkout | 0..* to 1 |
| Maintainer | starts the script in | Working Folder | 1 to 1 | | Maintainer | starts the script in | Working Folder | 1 to 1 |
| Checkout | holds by default | Configuration | 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..* | | Working Folder | is the base of | Local Project | 1 to 0..* |
## Generalizations ## Generalizations
@@ -83,3 +86,4 @@ WorkingFolder "1" --> "0..*" LocalProject : is the base of
[DM-001]: ../uc-001/dm.md [DM-001]: ../uc-001/dm.md
[DM-002]: ../domain-model.md [DM-002]: ../domain-model.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 | | 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 | 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 ## 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` | | Operation | `startFromWorkingFolder(configPath: Path [0..1], envPath: Path [0..1]): PromptSet` |
| Traces to | `startFromWorkingFolder` in [SSD-002] | | Traces to | `startFromWorkingFolder` in [SSD-002] |
| Concepts | Run, CommandLink, Checkout, WorkingFolder, Configuration | | Concepts | Run, CommandLink, Checkout, WorkingFolder, ConfigFiles, Configuration |
**Preconditions** **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. - 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`. - 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`. - 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. - 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. The `Run` was associated with a `PromptSet` that is returned. - 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** **Exceptions**
| Condition (failing precondition) | Outcome | | 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 | | 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 | | `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 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 | | 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 [DICT-001]: ../dictionary.md
[OC-001]: ../uc-001/oc.md [OC-001]: ../uc-001/oc.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 | | 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 | 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 ":ProjectCreator" as PC
participant ":Checkout" as CK participant ":Checkout" as CK
participant ":WorkingFolder" as WF participant ":WorkingFolder" as WF
participant ":ConfigFiles" as CF
Maintainer -> L : startFromWorkingFolder(configPath, envPath) Maintainer -> L : startFromWorkingFolder(configPath, envPath)
activate L activate L
@@ -37,13 +39,20 @@ L -> CK : Checkout(path)
L -> L : currentFolder() L -> L : currentFolder()
create WF create WF
L -> WF : WorkingFolder(path) 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 L --> Maintainer : error naming the folder looked in
end end
L -> PC : startProjectCreation(checkout, workingFolder, configPath, envPath) L -> PC : startProjectCreation(workingFolder, configFiles)
activate PC activate PC
alt config.env or .env not found, or a value malformed alt a value in config.env or .env is malformed
PC --> L : error naming the path or the key PC --> L : error naming the key
end end
PC --> L : promptSet PC --> L : promptSet
deactivate PC deactivate PC
@@ -56,9 +65,9 @@ deactivate L
| Pattern (GRASP / GoF) | Applied to | Rationale | | 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 | | 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 ### Postcondition Coverage
@@ -67,14 +76,15 @@ deactivate L
| P1 Run | `startFromWorkingFolder` (the run starts with it) | | P1 Run | `startFromWorkingFolder` (the run starts with it) |
| P2 Checkout | `resolveCheckout(invocation)` and the creation of `Checkout` | | P2 Checkout | `resolveCheckout(invocation)` and the creation of `Checkout` |
| P3 WorkingFolder | `currentFolder()` and the creation of `WorkingFolder` | | P3 WorkingFolder | `currentFolder()` and the creation of `WorkingFolder` |
| P4 default directory under the WorkingFolder | `startProjectCreation(checkout, workingFolder, ...)`; the prompt default is built from `workingFolder` | | P4 default directory under the WorkingFolder | `startProjectCreation(workingFolder, configFiles)`; 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]) | | P5 ConfigFiles | `locateConfigFiles(...)` and the creation of `ConfigFiles`; the paths are named before any request |
| P6 PromptSet | the returned `promptSet` | | P6 Configuration | `startProjectCreation(workingFolder, configFiles)` loads the two files (`load` of [SD-001]) |
| Exceptions: files not found; configuration not found or malformed | the two `alt` fragments | | P7 PromptSet | the returned `promptSet` |
| Exceptions: files not found, not confirmed, or malformed | the `alt` fragments |
### Responsibility Check ### 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 [DICT-001]: ../dictionary.md
[SD-001]: ../uc-001/sd.md [SD-001]: ../uc-001/sd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 | | 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 | 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 | | 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 | | 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 ## 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 [UC-002]: ./uc.md
[DM-003]: ./dm.md [DM-003]: ./dm.md
[OC-002]: ./oc.md [OC-002]: ./oc.md
[SSD-001]: ../uc-001/ssd.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 | | 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 | 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 - 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 - S03 — the README says exactly how to make the command global
- **Preconditions:** - **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. - A folder that is on the shell's `PATH` exists and the Maintainer may write to it.
- **Postconditions (success guarantee):** - **Postconditions (success guarantee):**
- A command link exists in a `PATH` folder and leads to the script in the checkout. - 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). 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). 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. 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. 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. 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. 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: - 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. 1. The system stops before any change and names the folder it looked in.
- 4b. `config.env` or `.env` is not found: - 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 the path it looked in and says that `--config` and `--env` can point elsewhere. 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 ### 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 | | 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 | 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 | | 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 | | 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 [SA-001]: ../stakeholder-analysis.md
[BC-001]: ../business-case.md [BC-001]: ../business-case.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [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 ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | 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 | 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** **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 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 `--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 missing file or a broken link, then the script stops before any change and names the folder or path it looked in. - 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. - 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 | | 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 [MIL-007]: ./milestones/mil-007-framework-checklists.md
[PP-001]: ./project-plan.md [PP-001]: ./project-plan.md
[1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f [1cd27f7]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/1cd27f77ed844773a969210a11de0d8bb98ac98f
[0ab5006]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/0ab50068bf9e5be82a801af9dbe5b763eeaf7f31
+39 -25
View File
@@ -10,10 +10,9 @@
# repositories and the mirror, then the local project: its directory, git # repositories and the mirror, then the local project: its directory, git
# repository, remotes (no credential in any address), the framework as a # repository, remotes (no credential in any address), the framework as a
# submodule, the framework's skills and git hooks (and the plan gate if # submodule, the framework's skills and git hooks (and the plan gate if
# chosen) and its templates. Choosing GitHub also applies the AGPL-3.0 # chosen) and its templates. The license of the Gitea repository is
# license to the Gitea repository. After a yes (default no) it also writes # PROJECT_LICENSE in config.env (none means no license); without it AGPL-3.0
# the new project's own .env with the credentials the project needs. No # applies only when GitHub is chosen and the project is public. No commit is made in the new project.
# commit is made in the new project.
# #
# Dry run by default # Dry run by default
# Without --apply the script only reads from GitHub and Gitea (GET # Without --apply the script only reads from GitHub and Gitea (GET
@@ -29,8 +28,7 @@
# Options # Options
# --apply create the repositories and the mirror (after a final yes) # --apply create the repositories and the mirror (after a final yes)
# --config FILE service addresses (default: config.env in the project root) # --config FILE service addresses (default: config.env in the project root)
# --env FILE credentials (default: .env in the project root); optional: # --env FILE credentials (default: .env in the project root)
# a credential it does not provide is asked, not echoed
# -h, --help show this help # -h, --help show this help
# --version show the version # --version show the version
# #
@@ -39,8 +37,7 @@
# the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL # the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL
# (default 10m0s) and FRAMEWORK_REPO (default # (default 10m0s) and FRAMEWORK_REPO (default
# TirSystem/SQA-QC-Framework, the submodule's OWNER/NAME) # TirSystem/SQA-QC-Framework, the submodule's OWNER/NAME)
# .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN (all optional, each # .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN
# asked when missing)
# #
# Environment # Environment
# REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry) # REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry)
@@ -69,8 +66,8 @@
# This file is the entry point. The work is split by responsibility into # This file is the entry point. The work is split by responsibility into
# the files in lib/ next to it (one job per file, see the first lines of # the files in lib/ next to it (one job per file, see the first lines of
# each file): constants, output, temp, util, validate, config, tools, json, # each file): constants, output, temp, util, validate, config, tools, json,
# http, api, prompts, credentials, project, hosts, preflight, steps, plan, # http, api, prompts, project, hosts, preflight, steps, plan, repositories,
# repositories, mirror, git, localproject, framework, envfile, apply and cli. The files are loaded # mirror, git, localproject, framework, apply and cli. The files are loaded
# from this directory only. # from this directory only.
# #
# Exit codes # Exit codes
@@ -89,18 +86,39 @@ if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4)));
fi fi
# Where this script lives; the library files and the project root are found # Where this script lives; the library files and the project root are found
# from here, never from the current directory. # from here, never from the current directory. A link to the script (a
readonly SCRIPT_FILE="${BASH_SOURCE[0]}" # command in a folder on PATH) is followed to the real file, however many
case "${BASH_SOURCE[0]}" in # links lie on the way; readlink without -f exists on Linux, macOS and Git
*/*) script_path_dir="${BASH_SOURCE[0]%/*}" ;; # Bash alike.
script_path="${BASH_SOURCE[0]}"
script_link_count=0
while [[ -L $script_path ]]; do
((++script_link_count <= 40)) || {
printf 'error: too many links when following %s\n' "${BASH_SOURCE[0]}" >&2
exit 1
}
script_link_target="$(readlink -- "$script_path")"
case "$script_link_target" in
/*) script_path="$script_link_target" ;;
*)
case "$script_path" in
*/*) script_path="${script_path%/*}/$script_link_target" ;;
*) script_path="$script_link_target" ;;
esac
;;
esac
done
readonly SCRIPT_FILE="$script_path"
case "$script_path" in
*/*) script_path_dir="${script_path%/*}" ;;
*) script_path_dir="." ;; *) script_path_dir="." ;;
esac esac
SCRIPT_DIR="$(cd "$script_path_dir" && pwd)" SCRIPT_DIR="$(cd -P "$script_path_dir" && pwd -P)"
readonly SCRIPT_DIR readonly SCRIPT_DIR
unset script_path_dir unset script_path script_path_dir script_link_count script_link_target
# The script lives in src/; the configuration files live one level up, in # The script lives in src/; the checkout is one level up, next to
# the project root, next to config.env.example and .env.example. # config.env.example and .env.example.
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd -P "$SCRIPT_DIR/.." && pwd -P)"
readonly PROJECT_ROOT readonly PROJECT_ROOT
# shellcheck source=lib/constants.sh # shellcheck source=lib/constants.sh
@@ -125,8 +143,6 @@ source "$SCRIPT_DIR/lib/http.sh"
source "$SCRIPT_DIR/lib/api.sh" source "$SCRIPT_DIR/lib/api.sh"
# shellcheck source=lib/prompts.sh # shellcheck source=lib/prompts.sh
source "$SCRIPT_DIR/lib/prompts.sh" source "$SCRIPT_DIR/lib/prompts.sh"
# shellcheck source=lib/credentials.sh
source "$SCRIPT_DIR/lib/credentials.sh"
# shellcheck source=lib/project.sh # shellcheck source=lib/project.sh
source "$SCRIPT_DIR/lib/project.sh" source "$SCRIPT_DIR/lib/project.sh"
# shellcheck source=lib/hosts.sh # shellcheck source=lib/hosts.sh
@@ -147,8 +163,6 @@ source "$SCRIPT_DIR/lib/git.sh"
source "$SCRIPT_DIR/lib/localproject.sh" source "$SCRIPT_DIR/lib/localproject.sh"
# shellcheck source=lib/framework.sh # shellcheck source=lib/framework.sh
source "$SCRIPT_DIR/lib/framework.sh" source "$SCRIPT_DIR/lib/framework.sh"
# shellcheck source=lib/envfile.sh
source "$SCRIPT_DIR/lib/envfile.sh"
# shellcheck source=lib/apply.sh # shellcheck source=lib/apply.sh
source "$SCRIPT_DIR/lib/apply.sh" source "$SCRIPT_DIR/lib/apply.sh"
# shellcheck source=lib/cli.sh # shellcheck source=lib/cli.sh
@@ -170,14 +184,14 @@ main() {
trap finish EXIT trap finish EXIT
is_valid_repo_name "$PROJECT_NAME" || is_valid_repo_name "$PROJECT_NAME" ||
die "REPOFOUNDRY_NAME is not a valid project name" die "REPOFOUNDRY_NAME is not a valid project name"
WORKING_FOLDER="$(pwd -P)"
parse_args "$@" parse_args "$@"
check_tools check_tools
setup_temp_dir setup_temp_dir
load_configuration load_configuration
collect_credentials GITEA_TOKEN
collect_project_details collect_project_details
if ((PROJECT[has_github])); then if ((PROJECT[has_github])); then
collect_credentials GITHUB_PAT GITHUB_USER require_github_credentials
fi fi
init_steps init_steps
print_summary print_summary
+2 -3
View File
@@ -19,7 +19,6 @@ create_all() {
add_framework add_framework
install_framework install_framework
copy_templates copy_templates
create_env_file
} }
# confirm_framework_access: the framework comes over SSH. Without SSH the # confirm_framework_access: the framework comes over SSH. Without SSH the
@@ -48,9 +47,9 @@ confirm_reuse() {
die "stopped: choose another name or remove the existing repository" die "stopped: choose another name or remove the existing repository"
fi fi
done done
if ((${STATE[reuse_gitea]:-0})) && ((PROJECT[has_github])) && if ((${STATE[reuse_gitea]:-0})) && [[ -n ${PROJECT[license]} ]] &&
[[ ${STATE[gitea_repo]} == empty ]]; then [[ ${STATE[gitea_repo]} == empty ]]; then
warn "the empty Gitea repository is reused as it is: the $AGPL_LICENSE_KEY license is not added to it" warn "the empty Gitea repository is reused as it is: the ${PROJECT[license]} license is not added to it"
fi fi
} }
+2
View File
@@ -9,6 +9,8 @@
usage() { usage() {
cat <<EOF cat <<EOF
Usage: ${0##*/} [--apply] [--config FILE] [--env FILE] Usage: ${0##*/} [--apply] [--config FILE] [--env FILE]
Without --config and --env, ./config.env and ./.env in the current folder
are read, then the ones in the checkout.
${0##*/} --help | --version ${0##*/} --help | --version
EOF EOF
} }
+67 -15
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: unquote_value, parse_env_file, parse_env_entry, validate_config, check_preset, check_preset_choice, validate_project_presets, validate_credentials, warn_if_env_unsafe, load_configuration # Provides: unquote_value, parse_env_file, parse_env_entry, validate_config, check_preset, check_preset_choice, validate_project_presets, validate_credentials, require_github_credentials, warn_if_env_unsafe, load_configuration
# unquote_value RAW: strip matching quotes (or a trailing " # comment" on an # unquote_value RAW: strip matching quotes (or a trailing " # comment" on an
# unquoted value) and return the value in REPLY. Fails on unbalanced quotes. # unquoted value) and return the value in REPLY. Fails on unbalanced quotes.
@@ -142,25 +142,23 @@ validate_project_presets() {
check_preset GITHUB_OWNER is_valid_github_owner "$HINT_GITHUB_OWNER" check_preset GITHUB_OWNER is_valid_github_owner "$HINT_GITHUB_OWNER"
check_preset PROJECT_DIRECTORY is_valid_directory "$HINT_DIRECTORY" check_preset PROJECT_DIRECTORY is_valid_directory "$HINT_DIRECTORY"
check_preset_choice ENABLE_PLAN_GATE yes no check_preset_choice ENABLE_PLAN_GATE yes no
check_preset PROJECT_LICENSE is_valid_license "$HINT_LICENSE"
} }
# validate_credentials: check the credentials that .env provides. A credential
# that is not provided is not an error: it is asked later (collect_credentials).
validate_credentials() { validate_credentials() {
# Register secrets first so that no later message can show them. if [[ -z ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then
if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then die "GITEA_TOKEN is missing in $ENV_FILE (see .env.example)"
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}")
fi fi
# Register secrets first so that no later message can show them.
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}")
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]]; then if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]]; then
SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}") SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}")
fi fi
if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]] && is_valid_token "${CREDENTIALS[GITEA_TOKEN]}" ||
! is_valid_token "${CREDENTIALS[GITEA_TOKEN]}"; then die "GITEA_TOKEN in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)"
die "GITEA_TOKEN in $ENV_FILE is not a valid token ($HINT_TOKEN)"
fi
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]] && if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]] &&
! is_valid_token "${CREDENTIALS[GITHUB_PAT]}"; then ! is_valid_token "${CREDENTIALS[GITHUB_PAT]}"; then
die "GITHUB_PAT in $ENV_FILE is not a valid token ($HINT_TOKEN)" die "GITHUB_PAT in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)"
fi fi
if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] && if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] &&
! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then ! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then
@@ -168,6 +166,16 @@ validate_credentials() {
fi fi
} }
# GitHub credentials are only needed when the Maintainer chose GitHub.
require_github_credentials() {
local key
for key in GITHUB_PAT GITHUB_USER; do
if [[ -z ${CREDENTIALS[$key]:-} ]]; then
die "GitHub was chosen but $key is missing in $ENV_FILE (see .env.example)"
fi
done
}
warn_if_env_unsafe() { warn_if_env_unsafe() {
local file="$1" dir mode local file="$1" dir mode
case "$(uname -s 2>/dev/null || true)" in case "$(uname -s 2>/dev/null || true)" in
@@ -190,13 +198,57 @@ warn_if_env_unsafe() {
fi fi
} }
# locate_config_file NAME FLAG FILE_VAR ORIGIN_VAR: the file named with FLAG;
# otherwise ./NAME in the working folder; otherwise NAME in the checkout.
# The origin is "named", "folder" or "checkout".
locate_config_file() {
local name="$1" flag="$2"
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"
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' ;;
folder) printf 'from the working folder' ;;
*) printf 'from the checkout' ;;
esac
}
# confirm_folder_files: a file taken from the working folder may point the
# Gitea address elsewhere and so send the token there, so it is named with
# the address and needs a yes (default no) before the first request.
confirm_folder_files() {
local files=()
[[ $CONFIG_ORIGIN == folder ]] && files+=("$CONFIG_FILE")
[[ $ENV_ORIGIN == folder ]] && files+=("$ENV_FILE")
((${#files[@]} > 0)) || return 0
say "The working folder supplies: ${files[*]}"
say "Gitea would be ${CONFIG[GITEA_URL]}; the token from the credentials file is sent there."
prompt_yes_no "Use ${files[*]}" n
((REPLY)) || die "stopped before any request: choose the files with --config and --env, or run from the checkout"
}
load_configuration() { load_configuration() {
locate_config_file config.env --config CONFIG_FILE CONFIG_ORIGIN
locate_config_file .env --env ENV_FILE ENV_ORIGIN
say "Config file : $CONFIG_FILE ($(origin_words "$CONFIG_ORIGIN"))"
say "Credentials file: $ENV_FILE ($(origin_words "$ENV_ORIGIN"))"
parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG
validate_config validate_config
# .env is optional: a credential it does not provide is asked. confirm_folder_files
if [[ -e $ENV_FILE ]]; then
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
warn_if_env_unsafe "$ENV_FILE"
fi
validate_credentials validate_credentials
warn_if_env_unsafe "$ENV_FILE"
} }
+12 -6
View File
@@ -18,6 +18,7 @@ readonly DEFAULT_MIRROR_INTERVAL="10m0s"
readonly DEFAULT_SSH_PORT=10022 readonly DEFAULT_SSH_PORT=10022
readonly DEFAULT_FRAMEWORK_REPO="TirSystem/SQA-QC-Framework" readonly DEFAULT_FRAMEWORK_REPO="TirSystem/SQA-QC-Framework"
readonly AGPL_LICENSE_KEY="AGPL-3.0" readonly AGPL_LICENSE_KEY="AGPL-3.0"
readonly NO_LICENSE_WORD="none"
readonly DEFAULT_BRANCH="main" readonly DEFAULT_BRANCH="main"
# What the prompts and the preset keys in config.env both tell the Maintainer # What the prompts and the preset keys in config.env both tell the Maintainer
# when a value is refused. # when a value is refused.
@@ -25,20 +26,25 @@ readonly HINT_REPO_NAME="use letters, digits, '.', '_' or '-' (at most 100), not
readonly HINT_DESCRIPTION="at most $MAX_DESCRIPTION_LENGTH characters and no control characters" readonly HINT_DESCRIPTION="at most $MAX_DESCRIPTION_LENGTH characters and no control characters"
readonly HINT_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)" readonly HINT_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)"
readonly HINT_GITHUB_OWNER="use letters, digits or '-' (at most 39)" readonly HINT_GITHUB_OWNER="use letters, digits or '-' (at most 39)"
readonly HINT_LICENSE="use a Gitea license key (letters, digits, '.', '+' or '-', at most 64), such as AGPL-3.0 or MIT, or none"
readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters" readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters"
readonly HINT_TOKEN="8 to 255 letters, digits or _ . ~ + / = -"
readonly ENV_FILE_NAME=".env"
readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror" readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
"Local project" "Framework" "Skills and hooks" "Templates" "Project .env") "Local project" "Framework" "Skills and hooks" "Templates")
# shellcheck disable=SC2034 # read through namerefs (parse_env_file) # shellcheck disable=SC2034 # read through namerefs (parse_env_file)
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
PROJECT_NAME PROJECT_DESCRIPTION PROJECT_VISIBILITY GITEA_OWNER USE_GITHUB PROJECT_NAME PROJECT_DESCRIPTION PROJECT_VISIBILITY GITEA_OWNER USE_GITHUB
GITHUB_OWNER PROJECT_DIRECTORY ENABLE_PLAN_GATE) GITHUB_OWNER PROJECT_DIRECTORY ENABLE_PLAN_GATE PROJECT_LICENSE)
readonly CREDENTIAL_KEYS=(GITHUB_PAT GITHUB_USER GITEA_TOKEN) readonly CREDENTIAL_KEYS=(GITHUB_PAT GITHUB_USER GITEA_TOKEN)
CONFIG_FILE="$PROJECT_ROOT/config.env" # The configuration files: named by --config and --env, or chosen by
ENV_FILE="$PROJECT_ROOT/.env" # locate_config_file. The origin is named, folder or checkout.
CONFIG_FILE=""
ENV_FILE=""
CONFIG_ORIGIN=""
ENV_ORIGIN=""
# The folder the Maintainer started the script in.
WORKING_FOLDER=""
TMP_DIR="" TMP_DIR=""
HAS_JQ=0 HAS_JQ=0
HTTP_STATUS=0 HTTP_STATUS=0
-42
View File
@@ -1,42 +0,0 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# credentials.sh - Asking for a credential that .env does not provide.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: credential_label, collect_credentials
# credential_label KEY: the name of a credential as the Maintainer sees it.
credential_label() {
case "$1" in
GITEA_TOKEN) printf 'Gitea access token' ;;
GITHUB_PAT) printf 'GitHub personal access token' ;;
GITHUB_USER) printf 'GitHub account name (the account the token belongs to)' ;;
*) printf '%s' "$1" ;;
esac
}
# collect_credentials KEY...: ask for each credential that is not already
# provided. A token is read without echo and registered as a secret at once,
# so no later message can show it; the GitHub account name is not secret and
# is read like any other answer. An empty value in .env counts as not provided.
collect_credentials() {
local key
for key in "$@"; do
if [[ -n ${CREDENTIALS[$key]:-} ]]; then
continue
fi
case "$key" in
GITHUB_USER)
prompt_value "$(credential_label "$key")" "" is_valid_github_owner \
"use letters, digits or '-' (at most 39)"
;;
*)
prompt_secret "$(credential_label "$key")" is_valid_token "$HINT_TOKEN"
SECRET_VALUES+=("$REPLY")
;;
esac
CREDENTIALS[$key]="$REPLY"
REPLY=""
done
}
-97
View File
@@ -1,97 +0,0 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# envfile.sh - The .env file of the new project: the one place a credential is written.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: env_file_keys, env_file_key_list, exclude_env_file, write_env_file, create_env_file
# env_file_keys: the credentials the new project needs, one per line: the
# Gitea token, and the GitHub token and account name when GitHub was chosen.
env_file_keys() {
printf '%s\n' GITEA_TOKEN
if ((PROJECT[has_github])); then
printf '%s\n' GITHUB_PAT GITHUB_USER
fi
}
# env_file_key_list: the same keys on one line, for messages.
env_file_key_list() {
local keys
keys="$(env_file_keys | tr '\n' ' ')"
printf '%s' "${keys% }"
}
# exclude_env_file DIR: make git ignore .env in DIR without touching a tracked
# file: the entry goes into .git/info/exclude, which is never committed. It
# does nothing when .env is already ignored.
exclude_env_file() {
local dir="$1" gitdir exclude
if git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME"; then
return 0
fi
gitdir="$(git_project "$dir" rev-parse --absolute-git-dir)"
exclude="$gitdir/info/exclude"
mkdir -p -- "$gitdir/info"
# Start on a fresh line when the file does not end with one.
if [[ -s $exclude && -n "$(tail -c 1 -- "$exclude")" ]]; then
printf '\n' >>"$exclude"
fi
printf '%s\n' "# RepoFoundry: the credentials file of this project" "$ENV_FILE_NAME" >>"$exclude"
git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME" ||
die "could not make git ignore $ENV_FILE_NAME in $dir; nothing was written to it"
}
# write_env_file DIR IS_REPLACE: write the credentials to DIR/.env. The file is
# created private (mode 600) from the start, never readable by others, even
# for a moment: it is written under umask 077 as a temporary file next to the
# target and moved into place. An existing file is only replaced when
# IS_REPLACE is 1, and a file that appears in the meantime is never replaced.
write_env_file() {
local dir="$1" is_replace="$2" target tmp key
target="$dir/$ENV_FILE_NAME"
tmp="$(umask 077 && mktemp "$dir/$ENV_FILE_NAME.XXXXXX")"
TEMP_FILES+=("$tmp")
{
while IFS= read -r key; do
printf '%s=%s\n' "$key" "${CREDENTIALS[$key]}"
done < <(env_file_keys)
} >"$tmp"
if ((is_replace)); then
mv -f -- "$tmp" "$target"
else
mv -n -- "$tmp" "$target"
if [[ -e $tmp ]]; then
die "$target appeared while it was being written; it was not replaced"
fi
fi
}
# create_env_file: the last step. Only after a yes (default no) is the .env
# written, and an existing one is only replaced after another yes. Nothing
# printed names a value, only the keys.
create_env_file() {
local label="Project .env" dir="${PROJECT[directory]}" keys is_replace=0
keys="$(env_file_key_list)"
begin_step "$label"
prompt_yes_no "Create a $ENV_FILE_NAME file in the project with the credentials it needs ($keys); only you can read it and git ignores it" n
if ! ((REPLY)); then
finish_step "$label" "skipped" "(you declined)"
return 0
fi
if git_project "$dir" ls-files --error-unmatch -- "$ENV_FILE_NAME" >/dev/null 2>&1; then
finish_step "$label" "skipped" "($ENV_FILE_NAME is tracked by git; it was not written)"
return 0
fi
if [[ -e $dir/$ENV_FILE_NAME || -L $dir/$ENV_FILE_NAME ]]; then
prompt_yes_no "$ENV_FILE_NAME already exists in the project. Replace it" n
if ! ((REPLY)); then
finish_step "$label" "kept" "(the existing $ENV_FILE_NAME was left as it was)"
return 0
fi
is_replace=1
fi
exclude_env_file "$dir"
write_env_file "$dir" "$is_replace"
finish_step "$label" "created" "($keys; only you can read it, git ignores it)"
}
+15 -1
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: is_framework_skipped, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates # Provides: is_framework_skipped, init_framework_submodules, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates
# is_framework_skipped: succeed when the framework steps are left out because # is_framework_skipped: succeed when the framework steps are left out because
# SSH to Gitea is not available (the Maintainer agreed to that). # SSH to Gitea is not available (the Maintainer agreed to that).
@@ -12,6 +12,18 @@ is_framework_skipped() {
[[ ${STATE[skip_framework]:-0} == 1 ]] [[ ${STATE[skip_framework]:-0} == 1 ]]
} }
# init_framework_submodules DIR: fetch the submodules the framework holds
# itself (the qc checklists). Without a submodule of its own this changes
# nothing. A failure names the command to run by hand.
init_framework_submodules() {
local dir="$1" err
make_temp_file
err="$REPLY"
if ! git_project "$dir" submodule update -q --init --recursive 2>"$err"; then
die "the framework was added but git could not fetch its own submodules (the qc checklists). Run in $dir: git submodule update --init --recursive. Git said: $(head -n 2 "$err" | tr '\n' ' ')"
fi
}
# add_framework: git submodule add of the framework as "framework". SSH is # add_framework: git submodule add of the framework as "framework". SSH is
# needed for it; a failure says how to test the access. # needed for it; a failure says how to test the access.
add_framework() { add_framework() {
@@ -27,6 +39,7 @@ add_framework() {
if [[ $existing != "$url" ]]; then if [[ $existing != "$url" ]]; then
die "'framework' already exists in $dir and is not the framework submodule ($url)" die "'framework' already exists in $dir and is not the framework submodule ($url)"
fi fi
init_framework_submodules "$dir"
finish_step "$label" "reused" "$url (already a submodule)" finish_step "$label" "reused" "$url (already a submodule)"
return 0 return 0
fi fi
@@ -35,6 +48,7 @@ add_framework() {
if ! git_project "$dir" submodule add -q "$url" framework 2>"$err"; then if ! git_project "$dir" submodule add -q "$url" framework 2>"$err"; then
die "git could not add the framework from $url. Check the SSH access first: ssh -p ${CONFIG[GITEA_SSH_PORT]} -T git@$(gitea_host). Git said: $(head -n 2 "$err" | tr '\n' ' ')" die "git could not add the framework from $url. Check the SSH access first: ssh -p ${CONFIG[GITEA_SSH_PORT]} -T git@$(gitea_host). Git said: $(head -n 2 "$err" | tr '\n' ' ')"
fi fi
init_framework_submodules "$dir"
finish_step "$label" "created" "$url" finish_step "$label" "created" "$url"
} }
+3 -1
View File
@@ -72,7 +72,7 @@ checkout_gitea_history() {
} }
# create_local_project: the directory, the git repository on main, the # create_local_project: the directory, the git repository on main, the
# remotes and, when GitHub is chosen, the license history. No commit is made. # remotes and, when a license applies, the license history. No commit is made.
create_local_project() { create_local_project() {
local label="Local project" dir="${PROJECT[directory]}" local label="Local project" dir="${PROJECT[directory]}"
begin_step "$label" begin_step "$label"
@@ -84,6 +84,8 @@ create_local_project() {
ensure_remote "$dir" origin "$(origin_url)" ensure_remote "$dir" origin "$(origin_url)"
if ((PROJECT[has_github])); then if ((PROJECT[has_github])); then
ensure_remote "$dir" github "$(github_remote_url)" ensure_remote "$dir" github "$(github_remote_url)"
fi
if [[ -n ${PROJECT[license]} ]]; then
checkout_gitea_history "$dir" checkout_gitea_history "$dir"
fi fi
finish_step "$label" "created" "$dir (origin over $(origin_protocol))" finish_step "$label" "created" "$dir (origin over $(origin_protocol))"
+2 -3
View File
@@ -22,8 +22,8 @@ print_plan() {
say "Plan:" say "Plan:"
if ((STATE[reuse_gitea])); then if ((STATE[reuse_gitea])); then
gitea_action="reuse the existing repository (you will be asked to confirm)" gitea_action="reuse the existing repository (you will be asked to confirm)"
elif ((PROJECT[has_github])); then elif [[ -n ${PROJECT[license]} ]]; then
gitea_action="create (${PROJECT[visibility]}) with the $AGPL_LICENSE_KEY license" gitea_action="create (${PROJECT[visibility]}) with the ${PROJECT[license]} license$(license_note)"
else else
gitea_action="create (${PROJECT[visibility]}), empty" gitea_action="create (${PROJECT[visibility]}), empty"
fi fi
@@ -54,5 +54,4 @@ print_plan() {
else else
say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")" say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")"
fi fi
say "$(printf ' %-18s: %s' "Project .env" "you are asked whether to create it ($(env_file_key_list))")"
} }
+4 -4
View File
@@ -23,8 +23,8 @@ check_gitea_organization() {
check_gitea_license() { check_gitea_license() {
api_call gitea GET /licenses api_call gitea GET /licenses
expect_status "cannot list the licenses of the Gitea server" 200 expect_status "cannot list the licenses of the Gitea server" 200
json_has_value "$HTTP_BODY_FILE" key "$AGPL_LICENSE_KEY" || json_has_value "$HTTP_BODY_FILE" key "${PROJECT[license]}" ||
die "the Gitea server does not offer the $AGPL_LICENSE_KEY license" die "the Gitea server does not offer the ${PROJECT[license]} license"
} }
# inspect_repository HOST: record in STATE[HOST_repo] whether the repository # inspect_repository HOST: record in STATE[HOST_repo] whether the repository
@@ -70,7 +70,7 @@ preflight_gitea() {
check_gitea_organization "$owner" "$login" check_gitea_organization "$owner" "$login"
STATE[gitea_owner_kind]="organization" STATE[gitea_owner_kind]="organization"
fi fi
if ((PROJECT[has_github])); then if [[ -n ${PROJECT[license]} ]]; then
check_gitea_license check_gitea_license
fi fi
inspect_repository gitea inspect_repository gitea
@@ -143,7 +143,7 @@ decide_existing_repositories() {
free) ;; free) ;;
empty) STATE[reuse_$host]=1 ;; empty) STATE[reuse_$host]=1 ;;
initial_only) initial_only)
if [[ $host == gitea ]] && ((PROJECT[has_github])); then if [[ $host == gitea && -n ${PROJECT[license]} ]]; then
STATE[reuse_$host]=1 STATE[reuse_$host]=1
else else
die "the $(host_label "$host") repository $owner/${PROJECT[name]} already exists and has content; choose another name or remove it first" die "the $(host_label "$host") repository $owner/${PROJECT[name]} already exists and has content; choose another name or remove it first"
+32 -3
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: preset_detail, collect_project_details, collect_github_details, source_note, yes_no, credential_state, print_summary # Provides: preset_detail, resolve_license, license_note, collect_project_details, collect_github_details, source_note, yes_no, credential_state, print_summary
# preset_detail KEY NAME: a detail set in config.env is used and not asked; # preset_detail KEY NAME: a detail set in config.env is used and not asked;
# the value is returned in REPLY and marked in PRESET[NAME]. # the value is returned in REPLY and marked in PRESET[NAME].
@@ -14,6 +14,33 @@ preset_detail() {
PRESET[$2]=1 PRESET[$2]=1
} }
# resolve_license: the license that applies, in PROJECT[license] (empty means
# none). PROJECT_LICENSE in config.env decides, and "none" means no license;
# without it AGPL-3.0 applies only when GitHub is chosen and the project is
# public. The license is never asked.
resolve_license() {
PROJECT[license]=""
if [[ -n ${CONFIG[PROJECT_LICENSE]+set} ]]; then
PRESET[license]=1
if [[ ${CONFIG[PROJECT_LICENSE],,} != "$NO_LICENSE_WORD" ]]; then
PROJECT[license]="${CONFIG[PROJECT_LICENSE]}"
fi
elif ((PROJECT[has_github])) && [[ ${PROJECT[visibility]} == public ]]; then
PROJECT[license]="$AGPL_LICENSE_KEY"
fi
}
# license_note: where the license on the Gitea repository comes from.
license_note() {
if [[ -n ${PRESET[license]:-} ]]; then
source_note license
elif [[ -n ${PROJECT[license]} ]]; then
printf ' (default: GitHub and a public project)'
elif ((PROJECT[has_github])); then
printf ' (no default for a private project)'
fi
}
# Ask for each project detail, except those set in config.env. # Ask for each project detail, except those set in config.env.
collect_project_details() { collect_project_details() {
preset_detail PROJECT_NAME name || preset_detail PROJECT_NAME name ||
@@ -29,6 +56,7 @@ collect_project_details() {
prompt_value "Gitea owner (user or organization)" "" is_valid_gitea_owner "$HINT_GITEA_OWNER" prompt_value "Gitea owner (user or organization)" "" is_valid_gitea_owner "$HINT_GITEA_OWNER"
PROJECT[gitea_owner]="$REPLY" PROJECT[gitea_owner]="$REPLY"
collect_github_details collect_github_details
resolve_license
preset_detail PROJECT_DIRECTORY directory || preset_detail PROJECT_DIRECTORY directory ||
prompt_value "Local directory" "./${PROJECT[name]}" is_valid_directory "$HINT_DIRECTORY" prompt_value "Local directory" "./${PROJECT[name]}" is_valid_directory "$HINT_DIRECTORY"
PROJECT[directory]="$REPLY" PROJECT[directory]="$REPLY"
@@ -46,7 +74,7 @@ collect_github_details() {
if preset_detail USE_GITHUB has_github; then if preset_detail USE_GITHUB has_github; then
[[ $REPLY == yes ]] && REPLY=1 || REPLY=0 [[ $REPLY == yes ]] && REPLY=1 || REPLY=0
else else
prompt_yes_no "Also create a GitHub repository (applies the AGPL license)" y prompt_yes_no "Also create a GitHub repository" y
fi fi
PROJECT[has_github]="$REPLY" PROJECT[has_github]="$REPLY"
PROJECT[github_owner]="" PROJECT[github_owner]=""
@@ -90,10 +118,11 @@ print_summary() {
say " Description : ${PROJECT[description]:-(none)}$(source_note description)" say " Description : ${PROJECT[description]:-(none)}$(source_note description)"
say " Gitea : ${CONFIG[GITEA_URL]}/${PROJECT[gitea_owner]}/${PROJECT[name]}$(source_note gitea_owner)" say " Gitea : ${CONFIG[GITEA_URL]}/${PROJECT[gitea_owner]}/${PROJECT[name]}$(source_note gitea_owner)"
if ((PROJECT[has_github])); then if ((PROJECT[has_github])); then
say " GitHub : ${CONFIG[GITHUB_WEB_URL]}/${PROJECT[github_owner]}/${PROJECT[name]} (AGPL license applied)$(source_note github_owner)" say " GitHub : ${CONFIG[GITHUB_WEB_URL]}/${PROJECT[github_owner]}/${PROJECT[name]}$(source_note github_owner)"
else else
say " GitHub : not used$(source_note has_github)" say " GitHub : not used$(source_note has_github)"
fi fi
say " License : ${PROJECT[license]:-none}$(license_note)"
say " Directory : ${PROJECT[directory]}$(source_note directory)" say " Directory : ${PROJECT[directory]}$(source_note directory)"
say " Plan gate : $(yes_no "${PROJECT[is_plan_gate_enabled]}")$(source_note is_plan_gate_enabled)" say " Plan gate : $(yes_no "${PROJECT[is_plan_gate_enabled]}")$(source_note is_plan_gate_enabled)"
say "Credentials : GITEA_TOKEN $(credential_state GITEA_TOKEN)," \ say "Credentials : GITEA_TOKEN $(credential_state GITEA_TOKEN)," \
+1 -22
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: prompt_value, prompt_secret, prompt_choice, prompt_yes_no # Provides: prompt_value, prompt_choice, prompt_yes_no
# prompt_value LABEL DEFAULT VALIDATOR HINT: ask until VALIDATOR accepts the # prompt_value LABEL DEFAULT VALIDATOR HINT: ask until VALIDATOR accepts the
# answer; the accepted answer is returned in REPLY. # answer; the accepted answer is returned in REPLY.
@@ -27,27 +27,6 @@ prompt_value() {
done done
} }
# prompt_secret LABEL VALIDATOR HINT: like prompt_value for a secret. What is
# typed is not shown (read -s) and a refused answer is never repeated in the
# message. An empty answer is refused; there is no default.
prompt_secret() {
local label="$1" validator="$2" hint="$3" answer
while true; do
printf '%s (input is hidden): ' "$label" >&2
IFS= read -rs answer || {
printf '\n' >&2
die "no input available for '$label'"
}
printf '\n' >&2 # the newline that hidden input did not echo
answer="$(trim "$answer")"
if [[ -n $answer ]] && "$validator" "$answer"; then
REPLY="$answer"
return 0
fi
warn "invalid $label: $hint"
done
}
# prompt_choice LABEL DEFAULT CHOICE...: the answer is returned in REPLY. # prompt_choice LABEL DEFAULT CHOICE...: the answer is returned in REPLY.
prompt_choice() { prompt_choice() {
local label="$1" default="$2" answer local label="$1" default="$2" answer
+2 -2
View File
@@ -18,8 +18,8 @@ repo_body() {
"${PROJECT[name]}" "$description" "$private" "${PROJECT[name]}" "$description" "$private"
return 0 return 0
fi fi
if ((PROJECT[has_github])); then if [[ -n ${PROJECT[license]} ]]; then
extra=',"auto_init":true,"license":"'"$AGPL_LICENSE_KEY"'"' extra=',"auto_init":true,"license":"'"${PROJECT[license]}"'"'
else else
extra=',"auto_init":false' extra=',"auto_init":false'
fi fi
+5
View File
@@ -58,6 +58,11 @@ is_valid_port() {
[[ $1 =~ ^[0-9]{1,5}$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535)) [[ $1 =~ ^[0-9]{1,5}$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535))
} }
# A Gitea license key such as AGPL-3.0 or MIT, or the word none.
is_valid_license() {
[[ -n $1 && ${#1} -le 64 && $1 =~ ^[A-Za-z0-9.+-]+$ ]]
}
# A Go duration such as 10m0s or 8h0m0s, the form Gitea expects. # A Go duration such as 10m0s or 8h0m0s, the form Gitea expects.
is_valid_interval() { is_valid_interval() {
[[ -n $1 && $1 =~ ^([0-9]+h)?([0-9]+m)?([0-9]+s)?$ ]] [[ -n $1 && $1 =~ ^([0-9]+h)?([0-9]+m)?([0-9]+s)?$ ]]
+16 -2
View File
@@ -21,6 +21,16 @@ readonly FAKE_GITHUB_PAT="ghpFAKEtoken1234567890"
# Answers to the prompts: name, description, visibility, Gitea owner, GitHub # Answers to the prompts: name, description, visibility, Gitea owner, GitHub
# yes or no, GitHub owner, directory, plan gate. # yes or no, GitHub owner, directory, plan gate.
readonly ANSWERS_GITHUB=$'my-app\nA test app\n\nTirSystem\ny\nacme-org\n\nn\n' readonly ANSWERS_GITHUB=$'my-app\nA test app\n\nTirSystem\ny\nacme-org\n\nn\n'
# The same, for a public project (the AGPL-3.0 default applies with GitHub).
readonly ANSWERS_GITHUB_PUBLIC=$'my-app
A test app
public
TirSystem
y
acme-org
n
'
readonly ANSWERS_GITEA_ONLY=$'my-app\n\n\nTirSystem\nn\n\nn\n' readonly ANSWERS_GITEA_ONLY=$'my-app\n\n\nTirSystem\nn\n\nn\n'
SHARED_REMOTES="" SHARED_REMOTES=""
@@ -110,12 +120,15 @@ write_gitconfig() {
insteadOf = https://git.example.test/ insteadOf = https://git.example.test/
[url "file://$WORK/remote/"] [url "file://$WORK/remote/"]
insteadOf = ssh://git@git.example.test:10022/ insteadOf = ssh://git@git.example.test:10022/
[url "file://$WORK/remote/"]
insteadOf = ssh://git@git.tirsystem.com:10022/
EOF EOF
} }
# ensure_shared_remotes: build, once per run of the suite, the local bare # ensure_shared_remotes: build, once per run of the suite, the local bare
# repositories that stand in for Gitea (TirSystem/my-app.git, holding the # repositories that stand in for Gitea (TirSystem/my-app.git, holding the
# license commit) and for the framework (a copy of the real one). # license commit), for the framework and for the checklists the framework
# holds as its own submodule (copies of the real ones).
ensure_shared_remotes() { ensure_shared_remotes() {
if [[ -n $SHARED_REMOTES && -d $SHARED_REMOTES ]]; then if [[ -n $SHARED_REMOTES && -d $SHARED_REMOTES ]]; then
return 0 return 0
@@ -123,6 +136,7 @@ ensure_shared_remotes() {
SHARED_REMOTES="$(mktemp -d "${TMPDIR:-/tmp}/repofoundry-remotes.XXXXXX")" SHARED_REMOTES="$(mktemp -d "${TMPDIR:-/tmp}/repofoundry-remotes.XXXXXX")"
mkdir -p "$SHARED_REMOTES/TirSystem" mkdir -p "$SHARED_REMOTES/TirSystem"
git clone -q --bare "$REPO_ROOT/framework" "$SHARED_REMOTES/TirSystem/SQA-QC-Framework.git" git clone -q --bare "$REPO_ROOT/framework" "$SHARED_REMOTES/TirSystem/SQA-QC-Framework.git"
git clone -q --bare "$REPO_ROOT/framework/qc" "$SHARED_REMOTES/TirSystem/SQA-QC-Checklists.git"
git init -q --bare "$SHARED_REMOTES/TirSystem/my-app.git" git init -q --bare "$SHARED_REMOTES/TirSystem/my-app.git"
git init -q "$SHARED_REMOTES/seed" git init -q "$SHARED_REMOTES/seed"
git -C "$SHARED_REMOTES/seed" symbolic-ref HEAD refs/heads/main git -C "$SHARED_REMOTES/seed" symbolic-ref HEAD refs/heads/main
@@ -153,7 +167,7 @@ setup_local_remotes() {
# first, then the now empty directories from the bottom up. # first, then the now empty directories from the bottom up.
remove_workdir() { remove_workdir() {
if [[ -n $WORK && -d $WORK ]]; then if [[ -n $WORK && -d $WORK ]]; then
find "$WORK" \( -type f -o -type p \) -delete find "$WORK" \( -type f -o -type p -o -type l \) -delete
find "$WORK" -depth -type d -exec rmdir {} + find "$WORK" -depth -type d -exec rmdir {} +
fi fi
WORK="" WORK=""
+13
View File
@@ -96,6 +96,7 @@ validate_credentials"
assert_status "$case_name" 1 "$STATUS" assert_status "$case_name" 1 "$STATUS"
assert_contains "$case_name message" "$ERR" "$expected" assert_contains "$case_name message" "$ERR" "$expected"
done <<'EOF' done <<'EOF'
no Gitea token|GITHUB_USER=octo\n|GITEA_TOKEN is missing
token too short|GITEA_TOKEN=short\n|not a valid token token too short|GITEA_TOKEN=short\n|not a valid token
token with a backslash|GITEA_TOKEN=abc\\defgh12345\n|not a valid token token with a backslash|GITEA_TOKEN=abc\\defgh12345\n|not a valid token
bad GitHub token|GITEA_TOKEN=abcdefgh12345\nGITHUB_PAT=bad token\n|GITHUB_PAT bad GitHub token|GITEA_TOKEN=abcdefgh12345\nGITHUB_PAT=bad token\n|GITHUB_PAT
@@ -103,6 +104,18 @@ bad GitHub user|GITEA_TOKEN=abcdefgh12345\nGITHUB_USER=-bad-\n|GITHUB_USER
EOF EOF
} }
test_github_credentials_required_only_when_chosen() {
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/e.env"
run_lib "" "parse_env_file \"$WORK/e.env\" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
echo no-github-ok
ENV_FILE=\"$WORK/e.env\"
require_github_credentials"
assert_contains "Gitea-only .env is valid" "$OUT" "no-github-ok"
assert_status "GitHub credentials missing" 1 "$STATUS"
assert_contains "names the missing key" "$ERR" "GITHUB_PAT is missing"
}
test_validators() { test_validators() {
local fn value expected local fn value expected
while IFS='|' read -r fn value expected; do while IFS='|' read -r fn value expected; do
-305
View File
@@ -1,305 +0,0 @@
#!/usr/bin/env bash
# test-credentials.sh - tests for the credentials that .env does not provide
# and for the .env file of the new project (MIL-005): they are asked without
# echo, the project .env is only written after a yes, owner-only and ignored
# by git, and no token appears anywhere else. Sourced by run-tests.sh.
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
readonly NL=$'\n'
# credentials_input: the answers of a run with no .env at all and GitHub
# chosen: the Gitea token, the details, then the GitHub token and account.
credentials_input() {
printf '%s' "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
}
run_dry_cred() {
run_cli "$1" --config "$WORK/config.env" --env "$WORK/.env"
}
run_apply_cred() {
run_cli "$1" --apply --config "$WORK/config.env" --env "$WORK/.env"
}
# without_env: remove the .env of the fixtures.
without_env() {
rm -f -- "$WORK/.env"
}
# env_mode FILE: the permission bits, empty where the platform has none.
env_mode() {
case "$(uname -s 2>/dev/null || true)" in
MINGW* | MSYS* | CYGWIN*) ;;
*) stat -c '%a' -- "$1" 2>/dev/null || stat -f '%Lp' -- "$1" 2>/dev/null || true ;;
esac
}
# ------------------------------------------------------------ prompt_secret
test_prompt_secret_asks_again_and_never_repeats_the_answer() {
run_lib $'short\n\nlongenoughtoken1\n' \
'prompt_secret "Gitea access token" is_valid_token "needs 8 characters"; echo "[$REPLY]"'
assert_status "valid answer found" 0 "$STATUS"
assert_eq "valid answer returned" "[longenoughtoken1]" "$OUT"
assert_contains "told why" "$ERR" "invalid Gitea access token: needs 8 characters"
assert_not_contains "refused answer not repeated" "$ERR" "short"
assert_contains "says the input is hidden" "$ERR" "(input is hidden)"
}
test_prompt_secret_stops_when_input_ends() {
run_lib "" 'prompt_secret "Gitea access token" is_valid_token "x" </dev/null'
assert_status "end of input" 1 "$STATUS"
assert_contains "message names the credential" "$ERR" "no input available for 'Gitea access token'"
}
# ------------------------------------------------------ collect_credentials
test_collect_credentials_asks_only_what_is_missing() {
run_lib "$FAKE_GITHUB_PAT${NL}octo-user$NL" \
'CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
collect_credentials GITEA_TOKEN GITHUB_PAT GITHUB_USER
printf "%s|%s\n" "${CREDENTIALS[GITHUB_USER]}" "${#SECRET_VALUES[@]}"'
assert_status "asked" 0 "$STATUS"
assert_eq "account name kept, one secret registered" "octo-user|1" "$OUT"
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token"
assert_contains "account asked" "$ERR" "GitHub account name"
assert_not_contains "Gitea token not asked" "$ERR" "Gitea access token"
assert_not_contains "no token shown" "$ERR$OUT" "$FAKE_GITHUB_PAT"
}
test_a_token_that_is_asked_is_registered_as_a_secret() {
run_lib "$FAKE_GITEA_TOKEN$NL" \
'collect_credentials GITEA_TOKEN
warn "the token is giteaFAKEtoken1234567890 here"'
assert_status "asked" 0 "$STATUS"
assert_not_contains "redacted in later messages" "$ERR" "$FAKE_GITEA_TOKEN"
}
test_an_empty_value_in_env_counts_as_not_provided() {
printf 'GITEA_TOKEN=\nGITHUB_USER=\n' >"$WORK/e.env"
run_lib "$FAKE_GITEA_TOKEN$NL" \
'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
collect_credentials GITEA_TOKEN
echo asked-ok'
assert_status "empty value tolerated" 0 "$STATUS"
assert_contains "asked instead" "$ERR" "Gitea access token"
}
test_validate_credentials_no_longer_requires_any() {
printf '# nothing\n' >"$WORK/e.env"
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
echo fine'
assert_status "no credential required" 0 "$STATUS"
assert_eq "no error" "fine" "$OUT"
printf 'GITEA_TOKEN=short\n' >"$WORK/e.env"
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials'
assert_status "a provided bad token is still refused" 1 "$STATUS"
assert_contains "named" "$ERR" "GITEA_TOKEN"
}
# --------------------------------------------------------------- the run
test_env_is_optional_and_the_token_is_asked_without_echo() {
setup_hosts
without_env
run_dry_cred "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
assert_status "dry run without .env" 0 "$STATUS"
assert_contains "token asked" "$ERR" "Gitea access token (input is hidden)"
assert_contains "plan printed" "$OUT" "Plan:"
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "only reads" "$(calls)" "POST"
}
test_a_provided_credential_is_not_asked() {
setup_hosts
run_dry_cred "$ANSWERS_GITHUB"
assert_status "all provided" 0 "$STATUS"
assert_not_contains "no token prompt" "$ERR" "(input is hidden)"
assert_not_contains "no account prompt" "$ERR" "GitHub account name"
}
test_github_credentials_are_asked_only_when_github_is_chosen() {
setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_dry_cred "$ANSWERS_GITEA_ONLY"
assert_status "Gitea only" 0 "$STATUS"
assert_not_contains "no GitHub token asked" "$ERR" "GitHub personal access token"
assert_not_contains "no GitHub account asked" "$ERR" "GitHub account name"
run_dry_cred "$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
assert_status "GitHub chosen" 0 "$STATUS"
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token (input is hidden)"
assert_contains "GitHub account asked" "$ERR" "GitHub account name"
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITHUB_PAT"
}
test_an_invalid_asked_value_is_asked_again_and_never_shown() {
setup_hosts
without_env
run_dry_cred "bad token${NL}$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
assert_status "second answer accepted" 0 "$STATUS"
assert_contains "told it is invalid" "$ERR" "invalid Gitea access token"
assert_not_contains "refused value not shown" "$ERR$OUT" "bad token"
}
test_input_that_ends_stops_before_any_request() {
setup_hosts
without_env
run_dry_cred ""
assert_status "stopped" 1 "$STATUS"
assert_contains "key named" "$ERR" "no input available for 'Gitea access token'"
assert_eq "no request made" "" "$(calls)"
assert_not_contains "no creation report" "$OUT" "This is what exists now"
}
test_asked_tokens_do_not_leak_under_bash_x() {
setup_hosts
without_env
STATUS=0
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" -x "$SCRIPT" \
--config "$WORK/config.env" --env "$WORK/.env" <<<"$(credentials_input)" \
>"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$?
assert_status "run under bash -x" 0 "$STATUS"
assert_not_contains "no Gitea token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITHUB_PAT"
}
# ---------------------------------------------------------- the project .env
test_the_dry_run_names_the_env_step_and_writes_nothing() {
setup_hosts
run_dry_cred "$ANSWERS_GITHUB"
assert_contains "plan line" "$OUT" "Project .env : you are asked whether to create it (GITEA_TOKEN GITHUB_PAT GITHUB_USER)"
assert_file_missing "no .env" "$WORK/my-app/.env"
}
test_the_project_env_is_not_created_without_a_yes() {
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y$NL$NL"
assert_status "run" 0 "$STATUS"
assert_file_missing "default is no" "$WORK/my-app/.env"
assert_contains "reported" "$OUT" "Project .env : skipped (you declined)"
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y${NL}n$NL"
assert_file_missing "explicit no" "$WORK/my-app/.env"
}
test_the_project_env_holds_only_the_needed_keys_and_is_private() {
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_file_exists ".env created" "$WORK/my-app/.env"
assert_eq "exactly the needed keys" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
assert_eq "owner-only" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
assert_contains "reported with the keys, not the values" "$OUT" "Project .env : created (GITEA_TOKEN GITHUB_PAT GITHUB_USER;"
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in the output" "$OUT$ERR" "$FAKE_GITHUB_PAT"
# Git ignores it without any tracked file changing.
check
if ! git -C "$WORK/my-app" check-ignore -q -- .env; then
fail ".env is not ignored by git"
fi
assert_not_contains "not listed by git status" "$(git -C "$WORK/my-app" status --porcelain)" ".env"
assert_contains "excluded locally" "$(cat "$WORK/my-app/.git/info/exclude")" ".env"
check
if [[ -e $WORK/my-app/.gitignore ]]; then
fail "a .gitignore was written; only .git/info/exclude may change"
fi
# No temporary file is left behind.
assert_eq "no leftover file" "" "$(find "$WORK/my-app" -maxdepth 1 -name '.env.*' -print)"
}
test_the_project_env_without_github_holds_only_the_gitea_token() {
setup_hosts
run_apply_cred "${ANSWERS_GITEA_ONLY}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_eq "one key" "GITEA_TOKEN=$FAKE_GITEA_TOKEN" "$(cat "$WORK/my-app/.env")"
assert_contains "reported" "$OUT" "Project .env : created (GITEA_TOKEN;"
}
test_asked_credentials_are_what_the_project_env_holds() {
setup_hosts
without_env
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_eq "the asked values" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
}
test_no_token_is_in_any_file_but_the_project_env() {
setup_hosts
without_env
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
local hits
# The new project (with its .git folder), the script's temporary directory
# and its output; the stub curl's own request log is not part of the product.
hits="$(grep -rIl -F -e "$FAKE_GITEA_TOKEN" -e "$FAKE_GITHUB_PAT" "$WORK/my-app" "$WORK/tmp" "$WORK/out.txt" "$WORK/err.txt" 2>/dev/null |
grep -v -e '/my-app/\.env$' || true)"
assert_eq "only the project .env holds a token" "" "$hits"
}
test_an_existing_env_is_kept_unless_the_maintainer_says_replace() {
setup_hosts
mkdir -p "$WORK/my-app"
printf 'keep\n' >"$WORK/my-app/.env"
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}n$NL"
assert_status "declined replacing" 0 "$STATUS"
assert_eq "unchanged" "keep" "$(cat "$WORK/my-app/.env")"
assert_contains "reported" "$OUT" "Project .env : kept (the existing .env was left as it was)"
remove_workdir
new_workdir
setup_hosts
mkdir -p "$WORK/my-app"
printf 'keep\n' >"$WORK/my-app/.env"
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}y$NL"
assert_status "agreed to replace" 0 "$STATUS"
assert_contains "replaced" "$(cat "$WORK/my-app/.env")" "GITEA_TOKEN=$FAKE_GITEA_TOKEN"
assert_eq "private after replacing" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
}
test_a_tracked_env_is_never_written() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf 'tracked\n' >"$WORK/p/.env"
git -C "$WORK/p" add .env
git -C "$WORK/p" -c user.name=t -c user.email=t@example.test commit -q -m init
run_lib "y$NL" \
'PROJECT[directory]="'"$WORK"'/p"; PROJECT[has_github]=0
CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
init_steps
create_env_file
echo "${STEP_STATUS["Project .env"]}"'
assert_status "run" 0 "$STATUS"
assert_eq "skipped" "skipped" "$OUT"
assert_eq "unchanged" "tracked" "$(cat "$WORK/p/.env")"
assert_contains "says why" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "skipped"
}
test_exclude_is_added_once_and_keeps_the_existing_entries() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf 'build/' >"$WORK/p/.git/info/exclude" # no trailing newline
run_lib "" \
'exclude_env_file "'"$WORK"'/p"
exclude_env_file "'"$WORK"'/p"
echo done'
assert_status "run" 0 "$STATUS"
local exclude
exclude="$(cat "$WORK/p/.git/info/exclude")"
assert_contains "old entry kept" "$exclude" "build/"
assert_eq "entry added once" "1" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude")"
assert_eq "old entry still on its own line" "1" "$(grep -c '^build/$' "$WORK/p/.git/info/exclude")"
}
test_exclude_does_nothing_when_env_is_already_ignored() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf '.env\n' >"$WORK/p/.gitignore"
run_lib "" 'exclude_env_file "'"$WORK"'/p"; echo done'
assert_status "run" 0 "$STATUS"
assert_eq "exclude file untouched" "0" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude" || true)"
}
+23 -9
View File
@@ -31,7 +31,7 @@ test_dry_run_prints_the_plan_and_only_reads() {
setup_hosts setup_hosts
run_dry "$ANSWERS_GITHUB" run_dry "$ANSWERS_GITHUB"
assert_status "dry run" 0 "$STATUS" assert_status "dry run" 0 "$STATUS"
assert_contains "Gitea plan" "$OUT" "Gitea repository : create (private) with the AGPL-3.0 license https://git.example.test/TirSystem/my-app" assert_contains "Gitea plan" "$OUT" "Gitea repository : create (private), empty https://git.example.test/TirSystem/my-app"
assert_contains "GitHub plan" "$OUT" "GitHub repository : create (private), empty https://github.com/acme-org/my-app" assert_contains "GitHub plan" "$OUT" "GitHub repository : create (private), empty https://github.com/acme-org/my-app"
assert_contains "mirror plan" "$OUT" "Push mirror : Gitea -> GitHub every 10m0s" assert_contains "mirror plan" "$OUT" "Push mirror : Gitea -> GitHub every 10m0s"
assert_contains "origin plan" "$OUT" "Local origin : will use SSH (the SSH test passed)" assert_contains "origin plan" "$OUT" "Local origin : will use SSH (the SSH test passed)"
@@ -148,14 +148,18 @@ test_github_owner_must_be_a_member() {
assert_contains "pending message" "$ERR" "membership of the GitHub organization 'acme-org' is not active" assert_contains "pending message" "$ERR" "membership of the GitHub organization 'acme-org' is not active"
} }
test_license_must_be_offered_when_github_is_chosen() { test_license_must_be_offered_when_a_public_project_has_github() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]' prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
run_apply "$ANSWERS_GITHUB" run_apply "$ANSWERS_GITHUB_PUBLIC"
assert_status "no AGPL" 1 "$STATUS" assert_status "no AGPL" 1 "$STATUS"
assert_contains "message" "$ERR" "does not offer the AGPL-3.0 license" assert_contains "message" "$ERR" "does not offer the AGPL-3.0 license"
assert_not_contains "nothing created" "$(calls)" "POST" assert_not_contains "nothing created" "$(calls)" "POST"
# Without GitHub no license is needed, so the same server is fine. # Without GitHub, or for a private project, no license is needed, so the same server is fine.
setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
run_dry "$ANSWERS_GITHUB"
assert_status "private with GitHub" 0 "$STATUS"
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]' prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
@@ -215,8 +219,7 @@ test_apply_sends_the_right_request_bodies() {
bodies="$(cat "$WORK/curl.bodies")" bodies="$(cat "$WORK/curl.bodies")"
assert_contains "GitHub name" "$bodies" '"name":"my-app"' assert_contains "GitHub name" "$bodies" '"name":"my-app"'
assert_contains "GitHub is created empty" "$bodies" '"private":true,"auto_init":false}' assert_contains "GitHub is created empty" "$bodies" '"private":true,"auto_init":false}'
assert_contains "Gitea gets the license" "$bodies" '"license":"AGPL-3.0"' assert_not_contains "a private project gets no license" "$bodies" '"license"'
assert_contains "Gitea is initialised with it" "$bodies" '"auto_init":true'
assert_contains "default branch" "$bodies" '"default_branch":"main"' assert_contains "default branch" "$bodies" '"default_branch":"main"'
assert_contains "description" "$bodies" '"description":"A test app"' assert_contains "description" "$bodies" '"description":"A test app"'
assert_contains "mirror target without credentials" "$bodies" '"remote_address":"https://github.com/acme-org/my-app.git"' assert_contains "mirror target without credentials" "$bodies" '"remote_address":"https://github.com/acme-org/my-app.git"'
@@ -239,6 +242,17 @@ test_apply_never_prints_or_passes_a_token() {
assert_contains "GitHub token in the private config" "$(cat "$WORK/curl.config")" "Authorization: Bearer $FAKE_GITHUB_PAT" assert_contains "GitHub token in the private config" "$(cat "$WORK/curl.config")" "Authorization: Bearer $FAKE_GITHUB_PAT"
} }
test_apply_sends_the_license_for_a_public_project_with_github() {
setup_hosts
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
local bodies
bodies="$(cat "$WORK/curl.bodies")"
assert_contains "Gitea gets the license" "$bodies" '"license":"AGPL-3.0"'
assert_contains "Gitea is initialised with it" "$bodies" '"auto_init":true'
assert_contains "public" "$bodies" '"private":false'
assert_contains "plan names the rule" "$OUT" "with the AGPL-3.0 license (default: GitHub and a public project)"
}
test_apply_with_gitea_only() { test_apply_with_gitea_only() {
setup_hosts setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
@@ -337,7 +351,7 @@ test_gitea_repository_with_only_the_license_can_be_reused() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}' prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}'
prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"LICENSE","type":"file","path":"LICENSE"}]' prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"LICENSE","type":"file","path":"LICENSE"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n' run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "license only" 0 "$STATUS" assert_status "license only" 0 "$STATUS"
assert_not_contains "Gitea repository not created again" "$(calls)" "$GITEA_REPO_CALL" assert_not_contains "Gitea repository not created again" "$(calls)" "$GITEA_REPO_CALL"
assert_eq "mirror created once" "1" "$(calls | grep -c -x -F "$MIRROR_CALL")" assert_eq "mirror created once" "1" "$(calls | grep -c -x -F "$MIRROR_CALL")"
@@ -355,7 +369,7 @@ test_gitea_repository_with_only_the_license_can_be_reused() {
test_reusing_an_empty_gitea_repository_warns_about_the_license() { test_reusing_an_empty_gitea_repository_warns_about_the_license() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":true}' prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":true}'
run_apply "$ANSWERS_GITHUB"$'y\ny\n' run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "reuse empty Gitea repository" 0 "$STATUS" assert_status "reuse empty Gitea repository" 0 "$STATUS"
assert_contains "warning" "$ERR" "the AGPL-3.0 license is not added to it" assert_contains "warning" "$ERR" "the AGPL-3.0 license is not added to it"
} }
@@ -485,7 +499,7 @@ test_a_repository_this_script_created_earlier_can_be_reused() {
setup_hosts setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}' prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}'
prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"README.md","type":"file"},{"name":"LICENSE","type":"file"}]' prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"README.md","type":"file"},{"name":"LICENSE","type":"file"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n' run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "LICENSE and README.md" 0 "$STATUS" assert_status "LICENSE and README.md" 0 "$STATUS"
assert_not_contains "not created again" "$(calls)" "$GITEA_REPO_CALL" assert_not_contains "not created again" "$(calls)" "$GITEA_REPO_CALL"
assert_contains "reported" "$OUT" "Gitea repository : reused" assert_contains "reported" "$OUT" "Gitea repository : reused"
+219
View File
@@ -0,0 +1,219 @@
#!/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_files_found_nowhere_stop_before_any_request_and_name_both_places() {
setup_hosts
make_checkout
make_folder
rm -f "$CHECKOUT/.env" "$WORK/curl.calls"
run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" "$ANSWERS_GITEA_ONLY"
assert_status "no .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" "--env FILE"
assert_eq "no request to any host" "" "$(calls)"
}
# ----------------------------------------------------------- a link to it
test_the_script_runs_through_a_link_and_creates_the_project_in_the_current_folder() {
setup_hosts
make_checkout
make_folder
local bin="$WORK/linkbin"
mkdir -p "$bin"
MSYS=winsymlinks:nativestrict ln -s "$CHECKOUT/src/create-project.sh" "$bin/repo-foundry" 2>/dev/null || true
if [[ ! -L $bin/repo-foundry ]]; then
printf 'skipped: this shell cannot make symbolic links\n'
return 0
fi
run_from "$bin/repo-foundry" "$FOLDER" "" --version
assert_status "version through the link" 0 "$STATUS"
assert_contains "found its files" "$OUT" "RepoFoundry"
run_from "$bin/repo-foundry" "$FOLDER" "$ANSWERS_GITEA_ONLY"$'y\n' --apply
assert_status "apply through the link" 0 "$STATUS"
assert_contains "the checkout's files" "$OUT" "/checkout/config.env (from the checkout)"
assert_file_exists "the project is in the current folder" "$FOLDER/my-app/.git"
assert_file_missing "not in the checkout" "$CHECKOUT/my-app"
}
test_a_link_to_a_link_is_followed() {
setup_hosts
make_checkout
make_folder
local bin="$WORK/linkbin"
mkdir -p "$bin"
MSYS=winsymlinks:nativestrict ln -s "$CHECKOUT/src/create-project.sh" "$bin/first" 2>/dev/null || true
[[ -L $bin/first ]] || {
printf 'skipped: this shell cannot make symbolic links\n'
return 0
}
(cd "$bin" && MSYS=winsymlinks:nativestrict ln -s first second)
run_from "$bin/second" "$FOLDER" "" --version
assert_status "second link" 0 "$STATUS"
}
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# test-license.sh - tests for the project license (MIL-006): PROJECT_LICENSE
# in config.env, the default (AGPL-3.0 only for a public project with GitHub),
# "none", invalid values and a license the server does not offer. Sourced by
# run-tests.sh.
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
readonly ANSWERS_GITEA_ONLY_PUBLIC=$'my-app\n\npublic\nTirSystem\nn\n\nn\n'
readonly MIT_ROUTE='GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"},{"key":"AGPL-3.0","name":"AGPL-3.0"}]'
# use_license LINE: add a line to config.env and make the server offer MIT.
use_license() {
printf '%s\n' "$1" >>"$WORK/config.env"
prepend_route "$MIT_ROUTE"
}
# gitea_only_env: a Gitea-only run needs no GitHub credentials.
gitea_only_env() {
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
}
# ------------------------------------------------------------- key is set
test_a_license_in_config_is_used_with_github_for_a_private_project() {
setup_hosts
use_license "PROJECT_LICENSE=MIT"
run_apply "$ANSWERS_GITHUB"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_contains "Gitea gets MIT" "$(cat "$WORK/curl.bodies")" '"license":"MIT"'
assert_not_contains "no AGPL" "$(cat "$WORK/curl.bodies")" "AGPL"
assert_contains "plan" "$OUT" "create (private) with the MIT license (from config.env)"
assert_contains "summary" "$OUT" "License : MIT (from config.env)"
}
test_a_license_in_config_is_used_without_github() {
setup_hosts
gitea_only_env
use_license "PROJECT_LICENSE=MIT"
run_apply "$ANSWERS_GITEA_ONLY"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_contains "Gitea gets MIT" "$(cat "$WORK/curl.bodies")" '"license":"MIT"'
assert_contains "initialised with it" "$(cat "$WORK/curl.bodies")" '"auto_init":true'
assert_not_contains "no GitHub call" "$(calls)" "api.github.com"
assert_contains "the server is asked" "$(calls)" "/licenses"
}
test_the_license_key_is_taken_in_any_case_for_none() {
setup_hosts
use_license "PROJECT_LICENSE=NONE"
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_not_contains "no license" "$(cat "$WORK/curl.bodies")" '"license"'
}
# ------------------------------------------------------------------- none
test_none_gives_no_license_even_for_a_public_project_with_github() {
setup_hosts
use_license "PROJECT_LICENSE=none"
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_not_contains "no license" "$(cat "$WORK/curl.bodies")" '"license"'
assert_not_contains "no license lookup" "$(calls)" "/licenses"
assert_contains "plan" "$OUT" "create (public), empty"
assert_contains "summary" "$OUT" "License : none (from config.env)"
}
# ----------------------------------------------------------------- absent
test_without_the_key_a_public_project_with_github_gets_agpl() {
setup_hosts
run_dry "$ANSWERS_GITHUB_PUBLIC"
assert_status "dry run" 0 "$STATUS"
assert_contains "plan" "$OUT" "with the AGPL-3.0 license (default: GitHub and a public project)"
assert_contains "summary" "$OUT" "License : AGPL-3.0 (default: GitHub and a public project)"
}
test_without_the_key_a_private_project_with_github_gets_no_license() {
setup_hosts
run_dry "$ANSWERS_GITHUB"
assert_status "dry run" 0 "$STATUS"
assert_contains "summary" "$OUT" "License : none (no default for a private project)"
assert_not_contains "no license lookup" "$(calls)" "/licenses"
}
test_without_the_key_a_project_without_github_gets_no_license() {
setup_hosts
gitea_only_env
run_dry "$ANSWERS_GITEA_ONLY_PUBLIC"
assert_status "dry run" 0 "$STATUS"
assert_contains "summary" "$OUT" "License : none"
assert_not_contains "no marker" "$OUT" "License : none ("
assert_not_contains "no license lookup" "$(calls)" "/licenses"
}
# ---------------------------------------------------- empty and invalid
test_an_empty_license_stops_before_any_request() {
setup_hosts
printf 'PROJECT_LICENSE=\n' >>"$WORK/config.env"
run_dry "$ANSWERS_GITHUB"
assert_status "empty" 1 "$STATUS"
assert_contains "key named" "$ERR" "PROJECT_LICENSE in"
assert_contains "says empty" "$ERR" "is empty"
assert_eq "no request to any host" "" "$(calls)"
}
test_an_invalid_license_stops_before_any_request_and_is_never_asked() {
local value
for value in 'bad license' 'MIT/2' 'MIT;rm' "$(printf 'a%.0s' {1..65})"; do
remove_workdir
new_workdir
setup_hosts
printf 'PROJECT_LICENSE=%s\n' "$value" >>"$WORK/config.env"
run_dry "$ANSWERS_GITHUB"
assert_status "invalid '$value'" 1 "$STATUS"
assert_contains "key named" "$ERR" "PROJECT_LICENSE in"
assert_eq "no request to any host" "" "$(calls)"
assert_not_contains "never asked" "$ERR" "License ("
done
}
# ------------------------------------------------------ not offered
test_a_license_the_server_does_not_offer_stops_before_anything_is_created() {
setup_hosts
use_license "PROJECT_LICENSE=Zlib"
run_apply "$ANSWERS_GITHUB"$'y\n'
assert_status "not offered" 1 "$STATUS"
assert_contains "license named" "$ERR" "does not offer the Zlib license"
assert_not_contains "nothing created" "$(calls)" "POST"
}
# ------------------------------------------------------- never asked
test_the_license_is_never_asked() {
setup_hosts
run_dry "$ANSWERS_GITHUB_PUBLIC"
assert_not_contains "no license prompt" "$ERR" "icense ("
assert_not_contains "no license prompt, any case" "$ERR" "License:"
remove_workdir
new_workdir
setup_hosts
use_license "PROJECT_LICENSE=MIT"
run_dry "$ANSWERS_GITHUB"
assert_not_contains "no license prompt with the key" "$ERR" "icense ("
}
# ------------------------------------------- mirror and local history
test_the_license_file_reaches_the_local_project_without_github() {
setup_hosts
gitea_only_env
use_license "PROJECT_LICENSE=MIT"
local dir="$WORK/project" answers
printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir"
run_apply "$answers"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_eq "the Gitea license commit is the whole history" "1" "$(GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir" rev-list --count HEAD)"
assert_file_exists "LICENSE from Gitea" "$dir/LICENSE"
}
+1 -1
View File
@@ -12,7 +12,7 @@
# LOCAL_ANSWERS (kept in a variable because $(...) would drop the last newline). # LOCAL_ANSWERS (kept in a variable because $(...) would drop the last newline).
local_answers() { local_answers() {
if [[ $2 == y ]]; then if [[ $2 == y ]]; then
printf -v LOCAL_ANSWERS 'my-app\nA test app\n\nTirSystem\ny\nacme-org\n%s\n%s\n' "$1" "$3" printf -v LOCAL_ANSWERS 'my-app\nA test app\npublic\nTirSystem\ny\nacme-org\n%s\n%s\n' "$1" "$3"
else else
printf -v LOCAL_ANSWERS 'my-app\n\n\nTirSystem\nn\n%s\n%s\n' "$1" "$3" printf -v LOCAL_ANSWERS 'my-app\n\n\nTirSystem\nn\n%s\n%s\n' "$1" "$3"
fi fi
+1 -1
View File
@@ -205,7 +205,7 @@ test_summary_marks_the_values_from_config_env() {
assert_contains "name" "$OUT" "Repository : my-app (from config.env) (private (from config.env))" assert_contains "name" "$OUT" "Repository : my-app (from config.env) (private (from config.env))"
assert_contains "description" "$OUT" "Description : A test app (from config.env)" assert_contains "description" "$OUT" "Description : A test app (from config.env)"
assert_contains "Gitea" "$OUT" "/TirSystem/my-app (from config.env)" assert_contains "Gitea" "$OUT" "/TirSystem/my-app (from config.env)"
assert_contains "GitHub" "$OUT" "(AGPL license applied) (from config.env)" assert_contains "GitHub" "$OUT" "/acme-org/my-app (from config.env)"
assert_contains "directory" "$OUT" "Directory : ./my-app (from config.env)" assert_contains "directory" "$OUT" "Directory : ./my-app (from config.env)"
assert_contains "plan gate" "$OUT" "Plan gate : no (from config.env)" assert_contains "plan gate" "$OUT" "Plan gate : no (from config.env)"
} }
+11 -9
View File
@@ -19,10 +19,10 @@ test_full_run_with_github() {
after="$(listing)" after="$(listing)"
assert_status "full run" 0 "$STATUS" assert_status "full run" 0 "$STATUS"
assert_contains "dry run" "$OUT" "Dry run: nothing was created" assert_contains "dry run" "$OUT" "Dry run: nothing was created"
assert_contains "plan" "$OUT" "create (private) with the AGPL-3.0 license" assert_contains "plan" "$OUT" "create (private), empty"
assert_contains "Gitea link derived from config" "$OUT" "https://git.example.test/TirSystem/my-app" assert_contains "Gitea link derived from config" "$OUT" "https://git.example.test/TirSystem/my-app"
assert_contains "GitHub link uses the chosen organization" "$OUT" "https://github.com/acme-org/my-app" assert_contains "GitHub link uses the chosen organization" "$OUT" "https://github.com/acme-org/my-app"
assert_contains "AGPL noted" "$OUT" "AGPL license applied" assert_contains "no default license for a private project" "$OUT" "License : none (no default for a private project)"
assert_contains "credential state" "$OUT" "GITEA_TOKEN set, GITHUB_PAT set" assert_contains "credential state" "$OUT" "GITEA_TOKEN set, GITHUB_PAT set"
assert_not_contains "no Gitea token in output" "$OUT$ERR" "$FAKE_GITEA_TOKEN" assert_not_contains "no Gitea token in output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in output" "$OUT$ERR" "$FAKE_GITHUB_PAT" assert_not_contains "no GitHub token in output" "$OUT$ERR" "$FAKE_GITHUB_PAT"
@@ -47,12 +47,12 @@ test_full_run_without_github() {
assert_contains "plan says GitHub is not used" "$OUT" "GitHub repository : not used" assert_contains "plan says GitHub is not used" "$OUT" "GitHub repository : not used"
} }
test_github_chosen_without_credentials_stops_when_input_ends() { test_github_chosen_without_credentials_fails() {
write_fixtures write_fixtures
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env" run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "missing GitHub credentials, no answer" 1 "$STATUS" assert_status "missing GitHub credentials" 1 "$STATUS"
assert_contains "names the credential" "$ERR" "no input available for 'GitHub personal access token'" assert_contains "names the key" "$ERR" "GITHUB_PAT is missing"
assert_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN" assert_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)" assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
} }
@@ -201,13 +201,15 @@ test_default_files_are_in_the_project_root() {
cp "$WORK/config.env" "$WORK/project/config.env" cp "$WORK/config.env" "$WORK/project/config.env"
cp "$WORK/.env" "$WORK/project/.env" cp "$WORK/.env" "$WORK/project/.env"
STATUS=0 STATUS=0
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ (cd "$WORK/project" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \
<<<"$ANSWERS_GITHUB" >"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$? <<<"$ANSWERS_GITHUB" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$?
assert_status "run with the default files" 0 "$STATUS" assert_status "run with the default files" 0 "$STATUS"
assert_contains "found config.env in the project root" "$(cat "$WORK/out.txt")" "https://git.example.test/TirSystem/my-app" assert_contains "found config.env in the project root" "$(cat "$WORK/out.txt")" "https://git.example.test/TirSystem/my-app"
# Run from another directory: the defaults follow the script, not the cwd. # Run from another folder that holds no files of its own: the checkout's
# files are used; the defaults follow the script, not the current folder.
mkdir -p "$WORK/elsewhere"
STATUS=0 STATUS=0
(cd "$WORK" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ (cd "$WORK/elsewhere" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \
<<<"$ANSWERS_GITEA_ONLY" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$? <<<"$ANSWERS_GITEA_ONLY" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$?
assert_status "run from another directory" 0 "$STATUS" assert_status "run from another directory" 0 "$STATUS"
} }