diff --git a/.github/workflows/post-release-verify.yml b/.github/workflows/post-release-verify.yml index 94a575f..9905dec 100644 --- a/.github/workflows/post-release-verify.yml +++ b/.github/workflows/post-release-verify.yml @@ -24,20 +24,30 @@ jobs: - name: Resolve release tag id: release + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - tag="${{ inputs.tag }}" + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_TAG" else - tag="${{ github.event.release.tag_name }}" + tag="$RELEASE_TAG" + fi + if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then + echo "::error::Invalid release tag; expected vMAJOR.MINOR.PATCH[-PRERELEASE]." + exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Download checksums env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} run: | mkdir -p dist/verify - gh release download "${{ steps.release.outputs.tag }}" \ + gh release download "$TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern checksums.txt \ --dir dist/verify @@ -73,20 +83,30 @@ jobs: steps: - name: Resolve release tag id: release + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - tag="${{ inputs.tag }}" + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_TAG" else - tag="${{ github.event.release.tag_name }}" + tag="$RELEASE_TAG" + fi + if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then + echo "::error::Invalid release tag; expected vMAJOR.MINOR.PATCH[-PRERELEASE]." + exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Download amd64 deb env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} run: | mkdir -p dist/verify - gh release download "${{ steps.release.outputs.tag }}" \ + gh release download "$TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern "*_amd64.deb" \ --dir dist/verify @@ -115,11 +135,20 @@ jobs: steps: - name: Resolve release tag id: release + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - tag="${{ inputs.tag }}" + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_TAG" else - tag="${{ github.event.release.tag_name }}" + tag="$RELEASE_TAG" + fi + if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then + echo "::error::Invalid release tag; expected vMAJOR.MINOR.PATCH[-PRERELEASE]." + exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" @@ -129,9 +158,10 @@ jobs: - name: Download amd64 rpm env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} run: | mkdir -p dist/verify - gh release download "${{ steps.release.outputs.tag }}" \ + gh release download "$TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern "*_amd64.rpm" \ --dir dist/verify @@ -188,11 +218,20 @@ jobs: - name: Resolve release tag id: release shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - tag="${{ inputs.tag }}" + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_TAG" else - tag="${{ github.event.release.tag_name }}" + tag="$RELEASE_TAG" + fi + if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then + echo "::error::Invalid release tag; expected vMAJOR.MINOR.PATCH[-PRERELEASE]." + exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" @@ -200,9 +239,10 @@ jobs: shell: bash env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} run: | mkdir -p dist/verify - gh release download "${{ steps.release.outputs.tag }}" \ + gh release download "$TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern "*_windows_amd64.zip" \ --dir dist/verify diff --git a/.github/workflows/sync-manifests.yml b/.github/workflows/sync-manifests.yml index e638fc7..dde78d1 100644 --- a/.github/workflows/sync-manifests.yml +++ b/.github/workflows/sync-manifests.yml @@ -27,26 +27,40 @@ jobs: uses: actions/checkout@v4 with: ref: main - # Use a PAT so the push step can bypass the branch protection rule - # that blocks GITHUB_TOKEN from pushing directly to main. - token: ${{ secrets.CI_PUSH_TOKEN }} - + # Don't write a token into .git/config: the branch-protection-bypass + # PAT (CI_PUSH_TOKEN) is handed only to the push command below. + persist-credentials: false + + # The tag is caller-chosen text (dispatch input or release name): bind it + # through env, never as an expression inside run:, and reject anything + # that isn't a plain semver tag before it reaches downloads, sed, or the + # manifests. - name: Resolve release tag id: release + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - tag="${{ inputs.tag }}" + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_TAG" else - tag="${{ github.event.release.tag_name }}" + tag="$RELEASE_TAG" + fi + if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then + echo "::error::Invalid release tag; expected vMAJOR.MINOR.PATCH[-PRERELEASE]." + exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Download checksums env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} run: | mkdir -p dist/render - gh release download "${{ steps.release.outputs.tag }}" \ + gh release download "$TAG" \ --repo "$GITHUB_REPOSITORY" \ --pattern checksums.txt \ --dir dist/render @@ -84,6 +98,9 @@ jobs: - name: Commit Homebrew formula and Scoop manifest env: TAG: ${{ steps.release.outputs.tag }} + # PAT that can bypass the branch protection rule blocking + # GITHUB_TOKEN from pushing directly to main. Scoped to this step. + CI_PUSH_TOKEN: ${{ secrets.CI_PUSH_TOKEN }} run: | set -euo pipefail @@ -100,7 +117,13 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "Update Homebrew formula and Scoop manifest for ${TAG}" - git push + auth="$(printf 'x-access-token:%s' "$CI_PUSH_TOKEN" | base64 -w0)" + echo "::add-mask::${auth}" + # Pass the auth header via env, not argv, so it isn't on the command line. + GIT_CONFIG_COUNT=1 \ + GIT_CONFIG_KEY_0="http.https://github.com/.extraheader" \ + GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${auth}" \ + git push origin HEAD:main - name: Submit WinGet manifests to winget-pkgs env: @@ -133,8 +156,19 @@ jobs: exit 1 fi - git clone --depth 1 \ - "https://x-access-token:${WINGET_GH_PAT}@github.com/${WINGET_FORK}.git" \ + # Supply the PAT as a per-command auth header so it is neither on the + # command line nor persisted in /tmp/winget-pkgs/.git/config. + winget_auth="$(printf 'x-access-token:%s' "$WINGET_GH_PAT" | base64 -w0)" + echo "::add-mask::${winget_auth}" + with_winget_auth() { + GIT_CONFIG_COUNT=1 \ + GIT_CONFIG_KEY_0="http.https://github.com/${WINGET_FORK}.git.extraheader" \ + GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${winget_auth}" \ + "$@" + } + + with_winget_auth git clone --depth 1 \ + "https://github.com/${WINGET_FORK}.git" \ /tmp/winget-pkgs cd /tmp/winget-pkgs @@ -151,7 +185,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "New version: DoiT.dci version ${version}" - git push -u origin "DoiT.dci-${version}" --force + with_winget_auth git push -u origin "DoiT.dci-${version}" --force # Create PR to microsoft/winget-pkgs fork_owner="${WINGET_FORK%%/*}" diff --git a/packaging/render.sh b/packaging/render.sh index 3a47120..a6bcc2f 100755 --- a/packaging/render.sh +++ b/packaging/render.sh @@ -14,11 +14,22 @@ base_url="$3" template="$4" output="$5" +# The values below are spliced into a sed program and into Ruby/JSON/YAML +# manifests, so restrict them to characters that are inert in all of those. +if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then + echo "ERROR: invalid version: expected MAJOR.MINOR.PATCH[-PRERELEASE]" >&2 + exit 1 +fi +if ! [[ "$base_url" =~ ^https://[A-Za-z0-9._~/-]+$ ]]; then + echo "ERROR: invalid base URL: expected https:// with only [A-Za-z0-9._~/-]" >&2 + exit 1 +fi + sha_for() { local file="$1" local hash - hash="$(grep " ${file}$" "$checksum_file" | awk '{print $1}')" - if [ -z "$hash" ]; then + hash="$(awk -v f="$file" '$2 == f {print $1}' "$checksum_file")" + if ! [[ "$hash" =~ ^[0-9a-f]{64}$ ]]; then echo "ERROR: checksum not found for ${file} in ${checksum_file}" >&2 exit 1 fi