A workspace alias resolves for Actions and for a token's reach, so g1t/deploy@v1 is flagon-io/deploy everywhere. uses: looks for an owner that is an alias (g1t for flagon-io) under its workspace's slug when nothing has the name as written, as the calling workspace sees it, and names what it finds by its own path; a g1t/ name answers ahead of GitHub only for a public repository and is never looked for on GitHub. The API decides a refused call again under the alias's workspace, so a token for flagon-io/deploy reaches it as g1t/deploy and nothing more. Actions release creates a missing g1t/<name> repository, public, through the API before pushing, and stops on a private one. The workspaces guide documents workspace aliases, and the actions guide g1t's own actions under the alias.
The site, the API and MCP, git over HTTPS and package registries already resolved aliases (identity's aliases.rs, migration 0029 seeds g1t for flagon-io); Actions and token reach were what still looked up g1t literally.
6 files+272−260/6 viewed
| 1 | 1 | # Publishes g1t's own actions: each folder of actions/ (deploy, detect…) | |
| 2 | 2 | # is pushed, as the whole of its own history, to the repository of the | |
| 3 | − | # same name in the g1t workspace on g1t.sh (actions/deploy to g1t/deploy), | |
| 4 | − | # and its major tag, v1, is moved to it. Workflows anywhere use them as | |
| 3 | + | # same name under g1t/ on g1t.sh (actions/deploy to g1t/deploy), and its | |
| 4 | + | # major tag, v1, is moved to it. Workflows anywhere use them as | |
| 5 | 5 | # `uses: g1t/deploy@v1`. A change to an action that breaks how it is | |
| 6 | 6 | # used gets a new major tag here (MAJOR below), and the old one stays. | |
| 7 | 7 | # | |
| ⋯ | |||
| 9 | 9 | # way (workflows/.g1t/workflows/deploy.yml is | |
| 10 | 10 | # `g1t/workflows/.g1t/workflows/deploy.yml@v1`). | |
| 11 | 11 | # | |
| 12 | + | # `g1t` is a workspace alias: on g1t.sh it is flagon-io's, so g1t/deploy | |
| 13 | + | # is the repository flagon-io/deploy, and the API, git and `uses:` all | |
| 14 | + | # answer for it under either name (see the workspaces guide, "Workspace | |
| 15 | + | # aliases"). This workflow names the alias throughout and never needs to | |
| 16 | + | # know which workspace it leads to. | |
| 17 | + | # | |
| 18 | + | # A repository that does not exist yet is created first, public, through | |
| 19 | + | # the API (POST /repos with `"workspace": "g1t"`, which the alias runs in | |
| 20 | + | # flagon-io); one that exists but is private stops the run, as nobody | |
| 21 | + | # outside its workspace could use it. | |
| 22 | + | # | |
| 12 | 23 | # Runs on a push to main that changes actions/ or workflows/, and by hand. | |
| 13 | 24 | # | |
| 14 | 25 | # Needs the repository secret G1T_ACTIONS_PUBLISH_TOKEN: an access token of | |
| 15 | − | # the g1t workspace with contents: write on its action repositories | |
| 16 | − | # (g1t/deploy, g1t/detect, and one for each new folder, made public | |
| 17 | − | # before its first release). The job's own token reaches this repository | |
| 18 | − | # only, so it cannot push there. | |
| 26 | + | # the workspace g1t leads to (flagon-io), with the scopes repo:write (to | |
| 27 | + | # create a missing repository) and code:write (to push and move tags). | |
| 28 | + | # A token without repo:write still publishes to repositories | |
| 29 | + | # that exist, and the run says which one to create by hand. The job's own | |
| 30 | + | # token reaches this repository only, so it can neither create nor push | |
| 31 | + | # to the others. | |
| 19 | 32 | name: Actions release | |
| 20 | 33 | ||
| 21 | 34 | on: | |
| ⋯ | |||
| 50 | 63 | env: | |
| 51 | 64 | PUBLISH_TOKEN: ${{ secrets.G1T_ACTIONS_PUBLISH_TOKEN }} | |
| 52 | 65 | SERVER: ${{ github.server_url }} | |
| 66 | + | API: ${{ github.api_url }} | |
| 53 | 67 | run: | | |
| 54 | 68 | set -euo pipefail | |
| 55 | 69 | if [ -z "${PUBLISH_TOKEN}" ]; then | |
| ⋯ | |||
| 58 | 72 | fi | |
| 59 | 73 | echo "::add-mask::${PUBLISH_TOKEN}" | |
| 60 | 74 | auth="Authorization: Basic $(printf 'g1t:%s' "${PUBLISH_TOKEN}" | base64 -w0)" | |
| 75 | + | bearer="Authorization: Bearer ${PUBLISH_TOKEN}" | |
| 61 | 76 | git config user.name g1t | |
| 62 | 77 | git config user.email g1t@users.noreply.g1t.sh | |
| 78 | + | ||
| 79 | + | # The repository g1t/<name>, public: created when it is missing. | |
| 80 | + | ensure() { | |
| 81 | + | local name="$1" description="$2" found status | |
| 82 | + | found="$(mktemp)" | |
| 83 | + | status="$(curl -sS -o "${found}" -w '%{http_code}' -H "${bearer}" "${API}/repos/${NAMESPACE}/${name}")" | |
| 84 | + | case "${status}" in | |
| 85 | + | 200) | |
| 86 | + | if [ "$(jq -r '.is_private' "${found}")" != "false" ]; then | |
| 87 | + | echo "::error::$(jq -r '.namespace + "/" + .name' "${found}") is private, so no workflow outside its workspace could use ${NAMESPACE}/${name}. Make it public, then run this again." | |
| 88 | + | exit 1 | |
| 89 | + | fi | |
| 90 | + | return 0 | |
| 91 | + | ;; | |
| 92 | + | 404) ;; | |
| 93 | + | *) | |
| 94 | + | echo "::error::Looking for ${NAMESPACE}/${name} answered ${status}: $(cat "${found}")" | |
| 95 | + | exit 1 | |
| 96 | + | ;; | |
| 97 | + | esac | |
| 98 | + | echo "Creating ${NAMESPACE}/${name}, public" | |
| 99 | + | status="$(jq -n --arg workspace "${NAMESPACE}" --arg name "${name}" --arg description "${description}" \ | |
| 100 | + | '{ workspace: $workspace, name: $name, description: $description, private: false }' \ | |
| 101 | + | | curl -sS -o "${found}" -w '%{http_code}' -X POST -H "${bearer}" -H "Content-Type: application/json" \ | |
| 102 | + | --data-binary @- "${API}/repos")" | |
| 103 | + | if [ "${status}" != "201" ] && [ "${status}" != "200" ]; then | |
| 104 | + | echo "::error::Creating ${NAMESPACE}/${name} answered ${status}: $(cat "${found}"). Give G1T_ACTIONS_PUBLISH_TOKEN the repo:write scope, or create ${NAMESPACE}/${name} once by hand, public." | |
| 105 | + | exit 1 | |
| 106 | + | fi | |
| 107 | + | echo "- Created \`$(jq -r '.namespace + "/" + .name' "${found}")\` for \`${NAMESPACE}/${name}\`" >> "${GITHUB_STEP_SUMMARY}" | |
| 108 | + | } | |
| 109 | + | ||
| 63 | 110 | for dir in actions/*/; do | |
| 64 | 111 | name="$(basename "${dir}")" | |
| 65 | 112 | [ -f "${dir}action.yml" ] || { echo "::warning::${dir} has no action.yml; skipped."; continue; } | |
| 113 | + | ensure "${name}" "$(sed -n 's/^description: *//p' "${dir}action.yml" | head -n 1 | tr -d "\"'")" | |
| 66 | 114 | commit="$(git subtree split --prefix "actions/${name}" HEAD)" | |
| 67 | 115 | remote="${SERVER}/${NAMESPACE}/${name}.git" | |
| 68 | 116 | echo "Publishing actions/${name} (${commit}) to ${NAMESPACE}/${name}" | |
| ⋯ | |||
| 72 | 120 | done | |
| 73 | 121 | # g1t's reusable workflows, from workflows/, as g1t/workflows. | |
| 74 | 122 | if [ -d workflows/.g1t/workflows ]; then | |
| 123 | + | ensure workflows "g1t's reusable workflows, such as .g1t/workflows/deploy.yml." | |
| 75 | 124 | commit="$(git subtree split --prefix workflows HEAD)" | |
| 76 | 125 | remote="${SERVER}/${NAMESPACE}/workflows.git" | |
| 77 | 126 | echo "Publishing workflows (${commit}) to ${NAMESPACE}/workflows" | |
| 140 | 140 | .find(|scope| !access.allows(*scope)) | |
| 141 | 141 | } | |
| 142 | 142 | ||
| 143 | + | /// Whether a decision refuses the call. | |
| 144 | + | fn refused(decision: &Option<Decision>) -> bool { | |
| 145 | + | decision.as_ref().is_some_and(|decision| !decision.allowed) | |
| 146 | + | } | |
| 147 | + | ||
| 143 | 148 | /// Runs `op` for `viewer`: enforcing an agent's scope first, and recording | |
| 144 | 149 | /// what happened. | |
| 145 | 150 | pub async fn run( | |
| ⋯ | |||
| 148 | 153 | viewer: &Viewer, | |
| 149 | 154 | input: &Value, | |
| 150 | 155 | ) -> Result<Outcome<Value>> { | |
| 151 | − | let decision = decide(op, services, viewer, input); | |
| 156 | + | let mut input = std::borrow::Cow::Borrowed(input); | |
| 157 | + | let mut decision = decide(op, services, viewer, &input); | |
| 158 | + | // A token or an agent's run that reaches one repository knows it by its | |
| 159 | + | // workspace's slug. A call that names it under the workspace's alias | |
| 160 | + | // (`g1t/deploy` for `flagon-io/deploy`) or an old slug is decided again, | |
| 161 | + | // and run, under the slug it resolves to; only a refusal pays for the | |
| 162 | + | // lookup, and the call is allowed only as the repository it is. | |
| 163 | + | if refused(&decision) | |
| 164 | + | && let Some(retargeted) = crate::renamed::retarget(services, &input).await? | |
| 165 | + | { | |
| 166 | + | let again = decide(op, services, viewer, &retargeted); | |
| 167 | + | if !refused(&again) { | |
| 168 | + | decision = again; | |
| 169 | + | input = std::borrow::Cow::Owned(retargeted); | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | let input = input.as_ref(); | |
| 152 | 173 | if let Some(decision) = decision.as_ref().filter(|decision| !decision.allowed) { | |
| 153 | 174 | record(op, services, viewer, input, decision, None).await; | |
| 154 | 175 | return Ok(Outcome::fail( | |
| ⋯ | |||
| 280 | 301 | } | |
| 281 | 302 | } | |
| 282 | 303 | ||
| 304 | + | /// A workflow job's token reaches its own repository, by its | |
| 305 | + | /// workspace's slug; named under the workspace's alias, the call is | |
| 306 | + | /// refused as written and decided again under the slug. | |
| 307 | + | #[test] | |
| 308 | + | fn a_repositorys_token_reaches_it_under_its_workspaces_alias() { | |
| 309 | + | let job = User { | |
| 310 | + | id: "usr_1".to_owned(), | |
| 311 | + | username: "syntaqx".to_owned(), | |
| 312 | + | token: Some(Box::new(TokenAccess { | |
| 313 | + | token_id: "tok_job".to_owned(), | |
| 314 | + | scopes: Some(vec![Scope::RepoRead.as_str().to_owned()]), | |
| 315 | + | repo: Some("flagon-io/deploy".to_owned()), | |
| 316 | + | ..TokenAccess::default() | |
| 317 | + | })), | |
| 318 | + | ..User::default() | |
| 319 | + | }; | |
| 320 | + | let aliased = json!({ "repo": "g1t/deploy" }); | |
| 321 | + | let first = token_decision(Op::GetRepo, &job, &aliased); | |
| 322 | + | assert!(refused(&first)); | |
| 323 | + | let retargeted = crate::renamed::rewrite(&aliased, "g1t", "flagon-io"); | |
| 324 | + | assert!(!refused(&token_decision(Op::GetRepo, &job, &retargeted))); | |
| 325 | + | // Another repository of the same workspace stays out of reach. | |
| 326 | + | let other = crate::renamed::rewrite(&json!({ "repo": "g1t/internal" }), "g1t", "flagon-io"); | |
| 327 | + | assert!(refused(&token_decision(Op::GetRepo, &job, &other))); | |
| 328 | + | assert!(!refused(&None)); | |
| 329 | + | } | |
| 330 | + | ||
| 283 | 331 | #[test] | |
| 284 | 332 | fn a_session_is_limited_only_by_the_person_s_role() { | |
| 285 | 333 | let person = User { id: "usr_1".to_owned(), ..User::default() }; | |
| 280 | 280 | ||
| 281 | 281 | | The repository | What happens | | |
| 282 | 282 | | --- | --- | | |
| 283 | − | | On g1t and public: in your workspace, under `g1t/`, or with a name GitHub has no public repository of | Your workflows use it, from any workspace. | | |
| 283 | + | | On g1t and public: in your workspace, under `g1t/` (g1t's own), or with a name GitHub has no public repository of | Your workflows use it, from any workspace. | | |
| 284 | 284 | | On g1t, private, in your workspace, with **Access** set to *Accessible from repositories in* the workspace | Your private repositories' workflows use it. A job reads it with a read-only token for that repository alone, which ends with the job. | | |
| 285 | 285 | | On g1t, private, and not shared that way | The step or job fails, and says why. A private repository's actions are never used by a public repository's workflows, whose logs anyone can read, nor from another workspace. | | |
| 286 | + | | Under `g1t/`, and not one of g1t's own public repositories | The step or job fails, and says so. A `g1t/` name is never looked for on GitHub. | | |
| 286 | 287 | | Not on g1t, or private in a workspace you cannot see | An action comes from GitHub, at the commit its ref names there (see [actions from GitHub](#actions-from-github)); a reusable workflow is read from a public repository on GitHub. | | |
| 287 | 288 | | On g1t, public, in another workspace, with a name GitHub has a public repository of | GitHub's is used, as on GitHub: anyone can make a workspace, so a workspace called `actions` never answers for `actions/setup-node`. When g1t can't reach GitHub to tell, GitHub's is used too. | | |
| 288 | 289 | ||
| 290 | + | An owner that is a [workspace alias](/guides/workspaces/#workspace-aliases) | |
| 291 | + | is looked for under its workspace's slug, by the same rules, as if you had | |
| 292 | + | written that slug: the alias decides where g1t looks, never who may use | |
| 293 | + | what it finds. | |
| 294 | + | ||
| 289 | 295 | `ref` is a branch, a tag or a commit. A reusable workflow may be under | |
| 290 | 296 | `.g1t/workflows/` or `.github/workflows/`; a `.github/workflows/` path | |
| 291 | 297 | also finds the file under `.g1t/workflows/` in a repository moved to | |
| ⋯ | |||
| 327 | 333 | ||
| 328 | 334 | ### g1t's own actions | |
| 329 | 335 | ||
| 330 | − | The `g1t` workspace on g1t.sh holds g1t's own actions, public, with a | |
| 331 | − | `v1` tag that moves with each compatible release: | |
| 336 | + | g1t's own actions live under `g1t/` on g1t.sh, public, with a `v1` tag | |
| 337 | + | that moves with each compatible release. `g1t` is a | |
| 338 | + | [workspace alias](/guides/workspaces/#workspace-aliases) of `flagon-io`, | |
| 339 | + | the workspace of Flagon, Inc., which builds g1t: `g1t/deploy@v1` is the | |
| 340 | + | repository `flagon-io/deploy`, and its page and the job's log name it | |
| 341 | + | that way. Write `g1t/`; it keeps working wherever g1t's actions are kept. | |
| 332 | 342 | ||
| 333 | 343 | | Action | What it does | | |
| 334 | 344 | | --- | --- | | |
| 335 | 345 | | [`g1t/deploy@v1`](https://g1t.sh/g1t/deploy) | Builds the checkout and puts it on [g1t.page](/guides/deployments/): production from the project's production branch, a preview from a pull request or another branch. The job needs `deployments: write`. See [deploying from a workflow](/guides/deployments/#deploying-from-a-workflow). | | |
| 336 | 346 | | [`g1t/detect@v1`](https://g1t.sh/g1t/detect) | Says what that build would find (`kind`, `install`, `build`, `output-dir`, `framework`) without building anything. | | |
| 337 | 347 | ||
| 338 | − | Both are composite actions around `g1t-runner`, which g1t's machines | |
| 348 | + | g1t's reusable workflows are in `g1t/workflows` the same way, such as | |
| 349 | + | `g1t/workflows/.g1t/workflows/deploy.yml@v1`, which a | |
| 350 | + | [project's](/guides/projects/) deployments run. | |
| 351 | + | ||
| 352 | + | A `g1t/` name always means g1t's own: it answers even where GitHub has a | |
| 353 | + | repository of the same name, only its public repositories answer for it, | |
| 354 | + | and a `g1t/` name that is not one of them fails rather than being looked | |
| 355 | + | for on GitHub. | |
| 356 | + | ||
| 357 | + | Both actions are composite actions around `g1t-runner`, which g1t's machines | |
| 339 | 358 | have; on a [self-hosted runner](/guides/self-hosted-runners/) they use | |
| 340 | 359 | the one on `PATH` or download the newest release. | |
| 341 | 360 | ||
| 158 | 158 | well-known GitHub action owner's name (such as `actions` or `docker`), and | |
| 159 | 159 | not another workspace's slug or someone else's username. | |
| 160 | 160 | ||
| 161 | + | ## Workspace aliases | |
| 162 | + | ||
| 163 | + | An alias is a second name for a workspace that leads to it for good, | |
| 164 | + | wherever a workspace's name goes. On g1t.sh, `g1t` is an alias of | |
| 165 | + | `flagon-io`, the workspace of Flagon, Inc., which builds g1t and holds its | |
| 166 | + | repositories: `g1t.sh/g1t/deploy` is the repository `flagon-io/deploy`. | |
| 167 | + | ||
| 168 | + | | | Behaviour | | |
| 169 | + | | --- | --- | | |
| 170 | + | | Web pages | Answer with a permanent redirect (301) to the same page under the workspace's slug, query string included. | | |
| 171 | + | | `git clone`, `fetch`, `pull` and `push` | Answered in place, as the repository under the workspace's slug, with the same permissions, and no redirect for git to warn about. | | |
| 172 | + | | API and MCP | A call that names the alias runs under the workspace's slug, and the answer names the workspace by its slug. A token that reaches one repository reaches it under the alias too, and nothing else. | | |
| 173 | + | | Packages | Registry addresses under the alias redirect to the workspace's. | | |
| 174 | + | | Actions | `uses: g1t/deploy@v1` and `g1t/workflows/.g1t/workflows/deploy.yml@v1` are found as `flagon-io/deploy` and `flagon-io/workflows`. See [g1t's own actions](/guides/actions/#g1ts-own-actions). | | |
| 175 | + | ||
| 176 | + | An alias resolves whatever its case (`G1T` is `g1t`), and follows the | |
| 177 | + | workspace through renames: it points at the workspace, not at its slug. | |
| 178 | + | Pages, API answers and links always show the workspace's own slug. | |
| 179 | + | ||
| 180 | + | An alias never changes who can see what. A private repository is found | |
| 181 | + | under the alias only by someone who could see it under the workspace's | |
| 182 | + | slug, and the alias gives nobody access they did not have. | |
| 183 | + | ||
| 184 | + | Aliases are set and removed by g1t's staff; there is no setting for them. | |
| 185 | + | While an alias exists, nobody can register it as a username, create a | |
| 186 | + | workspace with it, or rename a workspace to it. An alias is never a | |
| 187 | + | person's or a workspace's name, and never one of the site's own routes. | |
| 188 | + | [Contact us](https://g1t.sh/support) if your workspace needs one, such as | |
| 189 | + | for a name it used to trade under. | |
| 190 | + | ||
| 161 | 191 | ## Data residency | |
| 162 | 192 | ||
| 163 | 193 | Data residency says where the git data of the workspace's new repositories |
| 10 | 10 | //! private one's code). When g1t does not have it (or the workspace cannot | |
| 11 | 11 | //! see it), the action comes from GitHub, as before, and the reusable | |
| 12 | 12 | //! workflow from a public repository there. | |
| 13 | + | //! | |
| 14 | + | //! An owner that is a workspace alias g1t's staff set (identity's | |
| 15 | + | //! aliases.rs) is looked for under the workspace's own slug when nothing | |
| 16 | + | //! has the name as written: `g1t/deploy@v1` is `flagon-io/deploy`, as | |
| 17 | + | //! `g1t` is `flagon-io`'s alias. The repository found is named by its own | |
| 18 | + | //! path from then on (the job's download, the called workflow's origin), | |
| 19 | + | //! and is checked exactly as if it had been named that way: the alias | |
| 20 | + | //! changes where g1t looks, never who may see or use what it finds. | |
| 21 | + | //! | |
| 22 | + | //! `g1t/` names g1t's own actions. Nobody can register `g1t`, so such a | |
| 23 | + | //! repository answers for its name even where GitHub has one of the same | |
| 24 | + | //! name, but only when it is public; and a `g1t/` name g1t does not have | |
| 25 | + | //! (or that the workspace cannot see) is never taken from GitHub, where | |
| 26 | + | //! anyone might own `g1t`. | |
| 13 | 27 | ||
| 14 | 28 | use g1t_contracts::repos::{Repo, RepoPath}; | |
| 15 | 29 | use g1t_contracts::{FailureCode, Outcome, User}; | |
| ⋯ | |||
| 75 | 89 | Ok(()) | |
| 76 | 90 | } | |
| 77 | 91 | ||
| 92 | + | /// The owner that names g1t's own actions and workflows (`g1t/deploy`): | |
| 93 | + | /// a name nobody can register, an alias of the workspace that holds them | |
| 94 | + | /// (`flagon-io` on g1t.sh). | |
| 95 | + | pub(crate) const G1T_OWNER: &str = "g1t"; | |
| 96 | + | ||
| 97 | + | /// Whether `uses:` names one of g1t's own actions or workflows, by the | |
| 98 | + | /// owner as written. | |
| 99 | + | pub(crate) fn names_g1ts_own(owner: &str) -> bool { | |
| 100 | + | owner.eq_ignore_ascii_case(G1T_OWNER) | |
| 101 | + | } | |
| 102 | + | ||
| 78 | 103 | /// Whether `target` on g1t answers for its `owner/repo` name even when | |
| 79 | 104 | /// GitHub has a repository of the same name: when it is in the caller's own | |
| 80 | − | /// workspace, or is one of g1t's own (`g1t` is a name nobody can register). | |
| 81 | − | /// Any other workspace's repository answers only for names GitHub doesn't | |
| 82 | − | /// publish. | |
| 83 | − | pub(crate) fn answers_for_its_name(caller: &Repo, target: &Repo) -> bool { | |
| 84 | − | caller.namespace.eq_ignore_ascii_case(&target.namespace) || target.namespace.eq_ignore_ascii_case("g1t") | |
| 105 | + | /// workspace, or is one of g1t's own (named under `g1t/`, as written in | |
| 106 | + | /// `owner`, which reaches the workspace `g1t` is an alias of) and public. | |
| 107 | + | /// A private one of g1t's own is for its own workspace, as any private | |
| 108 | + | /// repository is. Any other workspace's repository answers only for names | |
| 109 | + | /// GitHub doesn't publish. | |
| 110 | + | pub(crate) fn answers_for_its_name(caller: &Repo, owner: &str, target: &Repo) -> bool { | |
| 111 | + | caller.namespace.eq_ignore_ascii_case(&target.namespace) || (names_g1ts_own(owner) && !target.is_private) | |
| 85 | 112 | } | |
| 86 | 113 | ||
| 114 | + | /// Whether an owner as written may be a workspace alias, and so is worth | |
| 115 | + | /// asking identity about: a namespace's shape in any case, as aliases have | |
| 116 | + | /// (identity reads it lowercased). | |
| 117 | + | pub(crate) fn may_be_alias(owner: &str) -> bool { | |
| 118 | + | g1t_contracts::aliasable_name(owner).is_some() | |
| 119 | + | } | |
| 120 | + | ||
| 87 | 121 | /// Where another repository's action or workflow comes from. | |
| 88 | 122 | pub(crate) enum Found { | |
| 89 | 123 | /// g1t has it, and the caller may use it. | |
| ⋯ | |||
| 96 | 130 | /// Looks for `owner/repo` on g1t, as `caller`'s workspace (`ws`) sees | |
| 97 | 131 | /// it, and checks the caller may use it. | |
| 98 | 132 | pub(crate) async fn find_used(&self, caller: &Repo, ws: &User, used: &UsesRef) -> Result<Outcome<Found>> { | |
| 133 | + | let viewer = Some(ws.clone()); | |
| 99 | 134 | let path = RepoPath { namespace: used.owner.clone(), name: used.repo.clone() }; | |
| 100 | − | let Some(target) = self.visible_repo(&path, &Some(ws.clone())).await? else { | |
| 135 | + | let mut found = self.visible_repo(&path, &viewer).await?; | |
| 136 | + | // Nothing has the name as written: an alias (`g1t` for `flagon-io`) | |
| 137 | + | // is looked for under its workspace's slug, as the same workspace | |
| 138 | + | // sees it, so a private repository there stays out of sight. | |
| 139 | + | if found.is_none() | |
| 140 | + | && let Some(slug) = self.alias_of(&used.owner).await? | |
| 141 | + | { | |
| 142 | + | found = self.visible_repo(&RepoPath { namespace: slug, name: used.repo.clone() }, &viewer).await?; | |
| 143 | + | } | |
| 144 | + | let Some(target) = found else { | |
| 145 | + | if names_g1ts_own(&used.owner) { | |
| 146 | + | return Ok(fail( | |
| 147 | + | FailureCode::NotFound, | |
| 148 | + | format!("{} is not one of g1t's actions or workflows: g1t's own are public repositories under g1t/.", used.full_name()), | |
| 149 | + | )); | |
| 150 | + | } | |
| 101 | 151 | return Ok(Outcome::Ok(Found::GitHub)); | |
| 102 | 152 | }; | |
| 103 | 153 | // A name GitHub publishes means GitHub's repository, unless the | |
| 104 | 154 | // repository on g1t is the caller's own workspace's or g1t's: anyone | |
| 105 | 155 | // can make a workspace called `actions`, and must not get to answer | |
| 106 | − | // for `actions/setup-node`. | |
| 107 | − | if !answers_for_its_name(caller, &target) && self.github_publishes(&used.full_name()).await? != Some(false) { | |
| 156 | + | // for `actions/setup-node`. A `g1t/` name is never GitHub's. | |
| 157 | + | if !answers_for_its_name(caller, &used.owner, &target) | |
| 158 | + | && !names_g1ts_own(&used.owner) | |
| 159 | + | && self.github_publishes(&used.full_name()).await? != Some(false) | |
| 160 | + | { | |
| 108 | 161 | return Ok(Outcome::Ok(Found::GitHub)); | |
| 109 | 162 | } | |
| 110 | 163 | let level = if target.is_private && target.id != caller.id { self.access_level_of(&target.id).await? } else { "none" }; | |
| ⋯ | |||
| 114 | 167 | }) | |
| 115 | 168 | } | |
| 116 | 169 | ||
| 170 | + | /// The slug of the workspace `owner` is an alias of, if it is one. | |
| 171 | + | async fn alias_of(&self, owner: &str) -> Result<Option<String>> { | |
| 172 | + | if !may_be_alias(owner) { | |
| 173 | + | return Ok(None); | |
| 174 | + | } | |
| 175 | + | let slug: Option<String> = | |
| 176 | + | g1t_kit::call(&self.identity, "resolve_alias", &g1t_contracts::identity::SlugArgs { slug: owner.to_owned() }).await?; | |
| 177 | + | Ok(slug.filter(|slug| !slug.eq_ignore_ascii_case(owner))) | |
| 178 | + | } | |
| 179 | + | ||
| 117 | 180 | /// `job_action`: a running job asks where to fetch an action from, by | |
| 118 | 181 | /// `report.repository` (`owner/repo`) and `report.ref`. Answers | |
| 119 | 182 | /// `{"source": "g1t", "url", "ref", "token"}` (a read-only token for | |
| ⋯ | |||
| 585 | 648 | fn only_its_own_workspace_or_g1t_answers_for_a_name_github_has() { | |
| 586 | 649 | let web = repo("web", "acme", true); | |
| 587 | 650 | // The caller's own workspace, and g1t's own actions, whatever GitHub has. | |
| 588 | − | assert!(answers_for_its_name(&web, &repo("setup", "acme", true))); | |
| 589 | − | assert!(answers_for_its_name(&web, &repo("setup", "ACME", false))); | |
| 590 | − | assert!(answers_for_its_name(&web, &repo("deploy", "g1t", false))); | |
| 651 | + | assert!(answers_for_its_name(&web, "acme", &repo("setup", "acme", true))); | |
| 652 | + | assert!(answers_for_its_name(&web, "acme", &repo("setup", "ACME", false))); | |
| 653 | + | assert!(answers_for_its_name(&web, "g1t", &repo("deploy", "g1t", false))); | |
| 591 | 654 | // Anyone else's public repository named like GitHub's `actions/setup-node` | |
| 592 | 655 | // does not: GitHub's is used when GitHub has one. | |
| 593 | − | assert!(!answers_for_its_name(&web, &repo("setup-node", "actions", false))); | |
| 656 | + | assert!(!answers_for_its_name(&web, "actions", &repo("setup-node", "actions", false))); | |
| 657 | + | } | |
| 658 | + | ||
| 659 | + | /// `g1t/deploy` is found as `flagon-io/deploy`, `g1t` being its alias. | |
| 660 | + | #[test] | |
| 661 | + | fn g1t_answers_through_its_alias_for_public_repositories_only() { | |
| 662 | + | let web = repo("web", "acme", true); | |
| 663 | + | let deploy = repo("deploy", "flagon-io", false); | |
| 664 | + | let internal = repo("internal", "flagon-io", true); | |
| 665 | + | // Written as g1t/deploy: g1t's own, public, answers for its name. | |
| 666 | + | assert!(answers_for_its_name(&web, "g1t", &deploy)); | |
| 667 | + | assert!(answers_for_its_name(&web, "G1T", &deploy)); | |
| 668 | + | // A private repository of the same workspace never does for another | |
| 669 | + | // workspace, and no other workspace may use it either way. | |
| 670 | + | assert!(!answers_for_its_name(&web, "g1t", &internal)); | |
| 671 | + | assert!(may_use(&web, &internal, "organization").unwrap_err().contains("only repositories in flagon-io")); | |
| 672 | + | assert!(may_use(&repo("site", "acme", false), &internal, "none").is_err()); | |
| 673 | + | // Named under flagon-io as written, it is only another workspace's. | |
| 674 | + | assert!(!answers_for_its_name(&web, "flagon-io", &deploy)); | |
| 675 | + | // flagon-io's own repositories use theirs as their own. | |
| 676 | + | let own = repo("g1t", "flagon-io", true); | |
| 677 | + | assert!(answers_for_its_name(&own, "g1t", &internal)); | |
| 678 | + | assert!(may_use(&own, &internal, "organization").is_ok()); | |
| 679 | + | // A public one of g1t's own is used from anywhere, public or private. | |
| 680 | + | assert!(may_use(&web, &deploy, "none").is_ok()); | |
| 681 | + | assert!(may_use(&repo("site", "acme", false), &deploy, "none").is_ok()); | |
| 682 | + | } | |
| 683 | + | ||
| 684 | + | #[test] | |
| 685 | + | fn only_namespace_shaped_owners_are_asked_about_as_aliases() { | |
| 686 | + | assert!(names_g1ts_own("g1t") && names_g1ts_own("G1t") && !names_g1ts_own("flagon-io")); | |
| 687 | + | assert!(may_be_alias("g1t")); | |
| 688 | + | assert!(may_be_alias("flagon") && may_be_alias("G1T")); | |
| 689 | + | for owner in ["some_org", "a.b", "-x", "x--y", "settings"] { | |
| 690 | + | assert!(!may_be_alias(owner), "{owner}"); | |
| 691 | + | } | |
| 594 | 692 | } | |
| 595 | 693 | } | |
| 10 | 10 | //! rename a workspace to it. | |
| 11 | 11 | //! | |
| 12 | 12 | //! An alias is resolved wherever an old slug is (`resolve_slug`): the site | |
| 13 | − | //! and the API redirect or run again under the workspace's slug. Git over | |
| 14 | − | //! HTTPS resolves it in place (`resolve_alias`), as pushes do not follow | |
| 15 | − | //! redirects. | |
| 13 | + | //! and the API redirect or run again under the workspace's slug (the API | |
| 14 | + | //! also decides a token's reach again under it). Git over HTTPS resolves | |
| 15 | + | //! it in place (`resolve_alias`), as pushes do not follow redirects, and | |
| 16 | + | //! so does Actions for `uses:` (its reach.rs: `g1t/deploy@v1` is | |
| 17 | + | //! `flagon-io/deploy`). | |
| 16 | 18 | ||
| 17 | 19 | use g1t_contracts::identity::*; | |
| 18 | 20 | use g1t_contracts::time::rfc3339; |
Comments
Sign in to comment.