| 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. |
| 32 | name: Actions release |
| 33 | |
| 34 | on: |
| 35 | push: |
| 36 | branches: [main] |
| 37 | paths: |
| 38 | - actions/** |
| 39 | - workflows/** |
| 40 | workflow_dispatch: |
| 41 | |
| 42 | permissions: |
| 43 | contents: read |
| 44 | |
| 45 | concurrency: |
| 46 | group: actions-release |
| 47 | cancel-in-progress: false |
| 48 | |
| 49 | env: |
| 50 | MAJOR: v1 |
| 51 | NAMESPACE: g1t |
| 52 | |
| 53 | jobs: |
| 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 |