fix(ci): authenticate releases through scoped credential helper
Release / release (push) Successful in 2m9s
CI / build (push) Successful in 51s

This commit is contained in:
dmg committed 2026-09-10 20:45:37 -04:00
1 parent 7a481f366c
commit 9d3791a9c3
4 files changed
+75 -14

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# Git credential protocol; never invoke directly with a real token in recorded output.
# Only Git receives credential stdout. No secret is stored in this file or passed in argv.
set -eu
[ "${1:-}" = get ] || exit 0
protocol='' host='' path=''
while IFS='=' read -r key value && [ -n "$key" ]; do
case "$key" in
protocol) protocol=$value ;;
host) host=$value ;;
path) path=$value ;;
esac
done
[ "$protocol" = https ] || exit 0
[ "$host" = git.garvis.dev ] || exit 0
[ "$path" = dmg/spigot-ignis.git ] || exit 0
[ -n "${IGNIS_RELEASE_TOKEN:-}" ] || exit 0
printf 'username=dmg\npassword=%s\n\n' "$IGNIS_RELEASE_TOKEN"
+6 -13
View File
@@ -48,20 +48,13 @@ jobs:
test -n "$IGNIS_RELEASE_TOKEN" || { echo 'RELEASE_TOKEN is required'; exit 1; }
test "$GITEA_SERVER_URL" = 'https://git.garvis.dev'
test "$REPOSITORY" = 'dmg/spigot-ignis'
# Token stays in environment/stdout to Git's protected askpass pipe, not a URL or argv.
askpass=$(mktemp)
trap 'rm -f "$askpass"' EXIT
chmod 700 "$askpass"
cat > "$askpass" <<'SH'
#!/bin/sh
case "$1" in
*Username*) printf '%s\n' 'x-access-token' ;;
*Password*) printf '%s\n' "$IGNIS_RELEASE_TOKEN" ;;
*) exit 1 ;;
esac
SH
export GIT_ASKPASS="$askpass" GIT_TERMINAL_PROMPT=0
# semantic-release overrides GIT_ASKPASS; use Git's credential protocol instead.
# Token stays in environment/credential stdout, never in the remote URL or argv.
export GIT_TERMINAL_PROMPT=0
git config credential.helper ''
git config --add credential.helper "$PWD/.gitea/scripts/git-credential-ignis.sh"
git config credential.useHttpPath true
trap 'git config --unset-all credential.helper || true; git config --unset credential.useHttpPath || true' EXIT
git remote set-url origin "${GITEA_SERVER_URL}/${REPOSITORY}.git"
previous=$(git describe --tags --abbrev=0 2>/dev/null || true)
npx -y \
@@ -0,0 +1,49 @@
package games.dmg.spigotignis;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
class ReleaseCredentialTest {
@Test
void credentialHelperRefusesOtherHostsProtocolsRepositoriesAndOperations() throws Exception {
Path helper = Path.of(".gitea/scripts/git-credential-ignis.sh").toAbsolutePath();
for (String request : java.util.List.of(
"protocol=https\nhost=evil.example\npath=dmg/spigot-ignis.git\n\n",
"protocol=http\nhost=git.garvis.dev\npath=dmg/spigot-ignis.git\n\n",
"protocol=https\nhost=git.garvis.dev\npath=dmg/other.git\n\n")) {
var builder = new ProcessBuilder("sh", helper.toString(), "get");
builder.environment().put("IGNIS_RELEASE_TOKEN", "synthetic-test-token");
var process = builder.start();
try (var input = process.getOutputStream()) { input.write(request.getBytes(StandardCharsets.UTF_8)); }
assertTrue(process.waitFor(3, TimeUnit.SECONDS));
assertEquals("", new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
}
var store = new ProcessBuilder("sh", helper.toString(), "store").start();
store.getOutputStream().close();
assertTrue(store.waitFor(3, TimeUnit.SECONDS));
assertEquals("", new String(store.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
}
@Test
void scopedHelperWorksEvenWhenSemanticReleaseOverridesAskpass() throws Exception {
Path helper = Path.of(".gitea/scripts/git-credential-ignis.sh").toAbsolutePath();
assertTrue(java.nio.file.Files.exists(helper), "A credential helper must survive semantic-release's askpass override");
var builder = new ProcessBuilder("git", "-c", "credential.helper=", "-c", "credential.helper=" + helper,
"-c", "credential.useHttpPath=true", "credential", "fill");
builder.environment().put("IGNIS_RELEASE_TOKEN", "synthetic-test-token");
builder.environment().put("GIT_ASKPASS", "echo");
builder.environment().put("GIT_TERMINAL_PROMPT", "0");
Process process = builder.start();
try (var input = process.getOutputStream()) {
input.write("protocol=https\nhost=git.garvis.dev\npath=dmg/spigot-ignis.git\n\n".getBytes(StandardCharsets.UTF_8));
}
assertTrue(process.waitFor(3, TimeUnit.SECONDS));
assertEquals(0, process.exitValue());
String result = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
assertTrue(result.contains("username=dmg"));
assertTrue(result.contains("password=synthetic-test-token"));
}
}
@@ -14,7 +14,8 @@ class ReleaseWorkflowTest {
String workflow = Files.readString(path);
assertTrue(workflow.contains("spigot-ignis-${VERSION}.jar"));
assertTrue(workflow.contains("--header @-"));
assertTrue(workflow.contains("GIT_ASKPASS"));
assertTrue(workflow.contains("git-credential-ignis.sh"));
assertTrue(workflow.contains("credential.useHttpPath true"));
assertTrue(workflow.contains("persist-credentials: false"));
assertFalse(workflow.contains("-H \"Authorization:"));
assertFalse(workflow.contains("GITHUB_TOKEN:"), "Do not pass a token for semantic-release to embed into git URLs");