Skip to content
135 linesCodeBlameRaw
1# Publishes g1t's own actions: each folder of actions/ (deploy, detect…)
2# is pushed, as the whole of its own history, to the repository of the
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# `uses: g1t/deploy@v1`. A change to an action that breaks how it is
6# used gets a new major tag here (MAJOR below), and the old one stays.
7#
8# g1t's reusable workflows, in workflows/, go to g1t/workflows the same
9# way (workflows/.g1t/workflows/deploy.yml is
10# `g1t/workflows/.g1t/workflows/deploy.yml@v1`).
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#
23# Runs on a push to main that changes actions/ or workflows/, and by hand.
24#
25# Needs the repository secret ACTIONS_PUBLISH_TOKEN: an access token of
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.
32name: Actions release
33
34on:
35 push:
36 branches: [main]
37 paths:
38 - actions/**
39 - workflows/**
40 workflow_dispatch:
41
42permissions:
43 contents: read
44
45concurrency:
46 group: actions-release
47 cancel-in-progress: false
48
49env:
50 MAJOR: v1
51 NAMESPACE: g1t
52
53jobs:
54 publish:
55 name: Publish g1t/<action>
56 runs-on: ubuntu-latest
57 timeout-minutes: 15
58 steps:
59 - uses: actions/checkout@v5
60 with:
61 fetch-depth: 0
62 # Without the job's own token left in git's config: it would go
63 # with every request to g1t.sh, beside the publish token below,
64 # and git sends the first, which may only write this repository,
65 # so the pushes to g1t/<name> were refused (403).
66 persist-credentials: false
67 - name: Push each action and move its major tag
68 env:
69 PUBLISH_TOKEN: ${{ secrets.ACTIONS_PUBLISH_TOKEN }}
70 SERVER: ${{ github.server_url }}
71 API: ${{ github.api_url }}
72 run: |
73 set -euo pipefail
74 if [ -z "${PUBLISH_TOKEN}" ]; then
75 echo "::error::Set the repository secret ACTIONS_PUBLISH_TOKEN (this workflow's header says what it needs)."
76 exit 1
77 fi
78 echo "::add-mask::${PUBLISH_TOKEN}"
79 auth="Authorization: Basic $(printf 'g1t:%s' "${PUBLISH_TOKEN}" | base64 -w0)"
80 bearer="Authorization: Bearer ${PUBLISH_TOKEN}"
81 git config user.name g1t
82 git config user.email g1t@users.noreply.g1t.sh
83
84 # The repository g1t/<name>, public: created when it is missing.
85 ensure() {
86 local name="$1" description="$2" found status
87 found="$(mktemp)"
88 status="$(curl -sS -o "${found}" -w '%{http_code}' -H "${bearer}" "${API}/repos/${NAMESPACE}/${name}")"
89 case "${status}" in
90 200)
91 if [ "$(jq -r '.is_private' "${found}")" != "false" ]; then
92 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."
93 exit 1
94 fi
95 return 0
96 ;;
97 404) ;;
98 *)
99 echo "::error::Looking for ${NAMESPACE}/${name} answered ${status}: $(cat "${found}")"
100 exit 1
101 ;;
102 esac
103 echo "Creating ${NAMESPACE}/${name}, public"
104 status="$(jq -n --arg workspace "${NAMESPACE}" --arg name "${name}" --arg description "${description}" \
105 '{ workspace: $workspace, name: $name, description: $description, private: false }' \
106 | curl -sS -o "${found}" -w '%{http_code}' -X POST -H "${bearer}" -H "Content-Type: application/json" \
107 --data-binary @- "${API}/repos")"
108 if [ "${status}" != "201" ] && [ "${status}" != "200" ]; then
109 echo "::error::Creating ${NAMESPACE}/${name} answered ${status}: $(cat "${found}"). Give ACTIONS_PUBLISH_TOKEN the repo:write scope, or create ${NAMESPACE}/${name} once by hand, public."
110 exit 1
111 fi
112 echo "- Created \`$(jq -r '.namespace + "/" + .name' "${found}")\` for \`${NAMESPACE}/${name}\`" >> "${GITHUB_STEP_SUMMARY}"
113 }
114
115 for dir in actions/*/; do
116 name="$(basename "${dir}")"
117 [ -f "${dir}action.yml" ] || { echo "::warning::${dir} has no action.yml; skipped."; continue; }
118 ensure "${name}" "$(sed -n 's/^description: *//p' "${dir}action.yml" | head -n 1 | tr -d "\"'")"
119 commit="$(git subtree split --prefix "actions/${name}" HEAD)"
120 remote="${SERVER}/${NAMESPACE}/${name}.git"
121 echo "Publishing actions/${name} (${commit}) to ${NAMESPACE}/${name}"
122 git -c http.extraHeader="${auth}" push "${remote}" "${commit}:refs/heads/main"
123 git -c http.extraHeader="${auth}" push --force "${remote}" "${commit}:refs/tags/${MAJOR}"
124 echo "- \`${NAMESPACE}/${name}@${MAJOR}\` is ${commit}" >> "${GITHUB_STEP_SUMMARY}"
125 done
126 # g1t's reusable workflows, from workflows/, as g1t/workflows.
127 if [ -d workflows/.g1t/workflows ]; then
128 ensure workflows "g1t's reusable workflows, such as .g1t/workflows/deploy.yml."
129 commit="$(git subtree split --prefix workflows HEAD)"
130 remote="${SERVER}/${NAMESPACE}/workflows.git"
131 echo "Publishing workflows (${commit}) to ${NAMESPACE}/workflows"
132 git -c http.extraHeader="${auth}" push "${remote}" "${commit}:refs/heads/main"
133 git -c http.extraHeader="${auth}" push --force "${remote}" "${commit}:refs/tags/${MAJOR}"
134 echo "- \`${NAMESPACE}/workflows@${MAJOR}\` is ${commit}" >> "${GITHUB_STEP_SUMMARY}"
135 fi