Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Merge workspace aliases through Actions and tokens: uses: g1t/deploy@v1 resolves to flagon-io/deploy, a token scoped to a repository reaches it under the alias, a g1t/ action is never taken from GitHub, and the Actions release creates g1t's action repositories when they are missing.

syntaqxcommitted Parentsd21288defdeefdBrowse files
6 files+272−260/6 viewed
+55−6
11 # Publishes g1t's own actions: each folder of actions/ (deploy, detect…)
22 # 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
55 # `uses: g1t/deploy@v1`. A change to an action that breaks how it is
66 # used gets a new major tag here (MAJOR below), and the old one stays.
77 #
99 # way (workflows/.g1t/workflows/deploy.yml is
1010 # `g1t/workflows/.g1t/workflows/deploy.yml@v1`).
1111 #
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+#
1223 # Runs on a push to main that changes actions/ or workflows/, and by hand.
1324 #
1425 # 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.
1932 name: Actions release
2033
2134 on:
5063 env:
5164 PUBLISH_TOKEN: ${{ secrets.G1T_ACTIONS_PUBLISH_TOKEN }}
5265 SERVER: ${{ github.server_url }}
66+ API: ${{ github.api_url }}
5367 run: |
5468 set -euo pipefail
5569 if [ -z "${PUBLISH_TOKEN}" ]; then
5872 fi
5973 echo "::add-mask::${PUBLISH_TOKEN}"
6074 auth="Authorization: Basic $(printf 'g1t:%s' "${PUBLISH_TOKEN}" | base64 -w0)"
75+ bearer="Authorization: Bearer ${PUBLISH_TOKEN}"
6176 git config user.name g1t
6277 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+
63110 for dir in actions/*/; do
64111 name="$(basename "${dir}")"
65112 [ -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 "\"'")"
66114 commit="$(git subtree split --prefix "actions/${name}" HEAD)"
67115 remote="${SERVER}/${NAMESPACE}/${name}.git"
68116 echo "Publishing actions/${name} (${commit}) to ${NAMESPACE}/${name}"
72120 done
73121 # g1t's reusable workflows, from workflows/, as g1t/workflows.
74122 if [ -d workflows/.g1t/workflows ]; then
123+ ensure workflows "g1t's reusable workflows, such as .g1t/workflows/deploy.yml."
75124 commit="$(git subtree split --prefix workflows HEAD)"
76125 remote="${SERVER}/${NAMESPACE}/workflows.git"
77126 echo "Publishing workflows (${commit}) to ${NAMESPACE}/workflows"
+49−1
140140 .find(|scope| !access.allows(*scope))
141141 }
142142
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+
143148 /// Runs `op` for `viewer`: enforcing an agent's scope first, and recording
144149 /// what happened.
145150 pub async fn run(
148153 viewer: &Viewer,
149154 input: &Value,
150155 ) -> 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();
152173 if let Some(decision) = decision.as_ref().filter(|decision| !decision.allowed) {
153174 record(op, services, viewer, input, decision, None).await;
154175 return Ok(Outcome::fail(
280301 }
281302 }
282303
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+
283331 #[test]
284332 fn a_session_is_limited_only_by_the_person_s_role() {
285333 let person = User { id: "usr_1".to_owned(), ..User::default() };
+23−4
280280
281281 | The repository | What happens |
282282 | --- | --- |
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. |
284284 | 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. |
285285 | 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. |
286287 | 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. |
287288 | 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. |
288289
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+
289295 `ref` is a branch, a tag or a commit. A reusable workflow may be under
290296 `.g1t/workflows/` or `.github/workflows/`; a `.github/workflows/` path
291297 also finds the file under `.g1t/workflows/` in a repository moved to
327333
328334 ### g1t's own actions
329335
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.
332342
333343 | Action | What it does |
334344 | --- | --- |
335345 | [`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). |
336346 | [`g1t/detect@v1`](https://g1t.sh/g1t/detect) | Says what that build would find (`kind`, `install`, `build`, `output-dir`, `framework`) without building anything. |
337347
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
339358 have; on a [self-hosted runner](/guides/self-hosted-runners/) they use
340359 the one on `PATH` or download the newest release.
341360
+30−0
158158 well-known GitHub action owner's name (such as `actions` or `docker`), and
159159 not another workspace's slug or someone else's username.
160160
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+
161191 ## Data residency
162192
163193 Data residency says where the git data of the workspace's new repositories
+110−12
1010 //! private one's code). When g1t does not have it (or the workspace cannot
1111 //! see it), the action comes from GitHub, as before, and the reusable
1212 //! 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`.
1327
1428 use g1t_contracts::repos::{Repo, RepoPath};
1529 use g1t_contracts::{FailureCode, Outcome, User};
7589 Ok(())
7690 }
7791
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+
78103 /// Whether `target` on g1t answers for its `owner/repo` name even when
79104 /// 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)
85112 }
86113
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+
87121 /// Where another repository's action or workflow comes from.
88122 pub(crate) enum Found {
89123 /// g1t has it, and the caller may use it.
96130 /// Looks for `owner/repo` on g1t, as `caller`'s workspace (`ws`) sees
97131 /// it, and checks the caller may use it.
98132 pub(crate) async fn find_used(&self, caller: &Repo, ws: &User, used: &UsesRef) -> Result<Outcome<Found>> {
133+ let viewer = Some(ws.clone());
99134 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+ }
101151 return Ok(Outcome::Ok(Found::GitHub));
102152 };
103153 // A name GitHub publishes means GitHub's repository, unless the
104154 // repository on g1t is the caller's own workspace's or g1t's: anyone
105155 // 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+ {
108161 return Ok(Outcome::Ok(Found::GitHub));
109162 }
110163 let level = if target.is_private && target.id != caller.id { self.access_level_of(&target.id).await? } else { "none" };
114167 })
115168 }
116169
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+
117180 /// `job_action`: a running job asks where to fetch an action from, by
118181 /// `report.repository` (`owner/repo`) and `report.ref`. Answers
119182 /// `{"source": "g1t", "url", "ref", "token"}` (a read-only token for
585648 fn only_its_own_workspace_or_g1t_answers_for_a_name_github_has() {
586649 let web = repo("web", "acme", true);
587650 // 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)));
591654 // Anyone else's public repository named like GitHub's `actions/setup-node`
592655 // 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+ }
594692 }
595693 }
+5−3
1010 //! rename a workspace to it.
1111 //!
1212 //! 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`).
1618
1719 use g1t_contracts::identity::*;
1820 use g1t_contracts::time::rfc3339;

Comments

Sign in to comment.