Skip to content

fix(storage): migrate GitHub artifact upload from deprecated v1-v3 API to v4#2612

Open
kiwamizamurai wants to merge 4 commits into
diggerhq:developfrom
kiwamizamurai:fix/github-artifacts-v4-upload
Open

fix(storage): migrate GitHub artifact upload from deprecated v1-v3 API to v4#2612
kiwamizamurai wants to merge 4 commits into
diggerhq:developfrom
kiwamizamurai:fix/github-artifacts-v4-upload

Conversation

@kiwamizamurai
Copy link
Copy Markdown

@kiwamizamurai kiwamizamurai commented Mar 15, 2026

Summary

Fixes #1702

The old StorePlanFile implementation called _apis/pipelines/workflows/{runID}/artifacts?api-version=6.0-preview (via ACTIONS_RUNTIME_URL), the deprecated Artifacts v1-v3 internal API. GitHub has removed this endpoint, causing HTTP 400 errors for anyone using upload-plan-destination: github.

This PR migrates the upload path to the Artifacts v4 Twirp/JSON protocol used by @actions/artifact v2+, the same protocol that actions/upload-artifact@v4 uses internally:

  1. POST {ACTIONS_RESULTS_URL}/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact → receive signed_upload_url
  2. PUT file contents to the signed Azure Blob URL (x-ms-blob-type: BlockBlob)
  3. POST .../FinalizeArtifact to commit the upload

The download path (DownloadLatestPlans, PlanExists, RetrievePlan) already uses the stable GitHub REST API (go-github) and is unchanged.

Changes

This PR contains three commits, each addressing a distinct piece needed to make v4 upload work:

1. libs/storage/plan_storage.go — replace v1-v3 calls with v4 Twirp/JSON

The download path is unchanged. Only StorePlanFile is rewritten.

2. action.yml — expose ACTIONS_RESULTS_URL to the digger CLI

GitHub Actions does not inject the runtime service env vars (ACTIONS_RUNTIME_TOKEN, ACTIONS_RESULTS_URL, etc.) into shell run: steps — they are only injected into Node.js JavaScript actions. The digger CLI is launched from a run: step, so it sees these variables as empty.

action.yml already had a Node.js step that re-exports the v1-v3 vars (ACTIONS_RUNTIME_URL, ACTIONS_RUNTIME_TOKEN, ACTIONS_CACHE_URL) via core.exportVariable() for exactly this reason. This PR adds ACTIONS_RESULTS_URL to that list (one-line change). Without this, the Go code aborts immediately with ACTIONS_RESULTS_URL is not set.

3. libs/storage/plan_storage.go — derive backend IDs from the JWT, not from env vars

The Twirp service rejects requests with HTTP 401 "invalid auth token" when workflow_run_backend_id is set to GITHUB_RUN_ID. The official @actions/artifact v2 package does not use GITHUB_RUN_ID / GITHUB_RUN_ATTEMPT — it parses the JWT in ACTIONS_RUNTIME_TOKEN and reads the scp claim, which has the form:

Actions.Results:<workflow_run_backend_id>:<workflow_job_run_backend_id>

The IDs the backend ties to the bearer token are baked into the token itself, so they must be extracted from the JWT — not guessed from env vars. This PR adds a small base64-decode-only JWT payload parser (no signature verification — the token is the runner's own) and uses the extracted IDs in CreateArtifact and FinalizeArtifact.

Test plan

End-to-end verified with a real PR on a GitHub-hosted runner: kiwamizamurai/digger_tutorial#4

Successful run output:

level=INFO msg="Successfully stored plan file in GitHub artifacts"
  owner=kiwamizamurai repo=digger_tutorial artifactName=sandbox size=2607

All three Twirp steps succeeded (CreateArtifact → signed-URL PUTFinalizeArtifact).

  • go build ./libs/storage/... passes
  • CreateArtifact returns signed_upload_url
  • Azure Blob PUT succeeds
  • FinalizeArtifact commits the artifact
  • Plan can be downloaded by a subsequent step (existing REST path is untouched)

Note: Twirp supports both binary protobuf and JSON encoding. This implementation uses JSON to avoid adding a protobuf dependency.

…I to v4

The old implementation used the internal `_apis/pipelines/workflows/{runID}/artifacts?api-version=6.0-preview`
endpoint (via ACTIONS_RUNTIME_URL), which GitHub deprecated and removed, causing HTTP 400 errors.

Replace with the Artifacts v4 Twirp/JSON protocol (ACTIONS_RESULTS_URL based):
1. POST CreateArtifact → receive signed_upload_url
2. PUT file contents to the signed Azure Blob URL
3. POST FinalizeArtifact to commit the upload

Closes diggerhq#1702

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kiwamizamurai kiwamizamurai changed the base branch from main to develop March 15, 2026 12:56
@s1ntaxe770r s1ntaxe770r reopened this Apr 24, 2026
@s1ntaxe770r
Copy link
Copy Markdown
Contributor

hey @kiwamizamurai ,

any chance you could link me to a repo where you tested this? or some screenshots

@kiwamizamurai
Copy link
Copy Markdown
Author

@s1ntaxe770r sorry I don't have it because I was working on it for long time ago....

@s1ntaxe770r
Copy link
Copy Markdown
Contributor

ahh, gotcha. Mind testing this out? This looks good but i would need to be certain it woks as intended

… upload

The new v4 artifact upload code in plan_storage.go reads ACTIONS_RESULTS_URL,
but GitHub Actions does not pass this internal env var to shell `run:` steps -
only to Node.js JavaScript actions. Without this passthrough, the Go binary
sees the variable as empty and aborts with:

    Error storing artifact file: ACTIONS_RESULTS_URL is not set

The existing actions/github-script step already exports the v1-v3 vars
(ACTIONS_RUNTIME_URL, ACTIONS_RUNTIME_TOKEN); this adds ACTIONS_RESULTS_URL
alongside them so the Go binary can reach it via os.Getenv.
…vars

The Artifacts v4 Twirp API rejects requests with HTTP 401 "invalid auth token"
when workflow_run_backend_id is set to GITHUB_RUN_ID. The official
@actions/artifact v2 package does NOT use those env vars; it parses the JWT
in ACTIONS_RUNTIME_TOKEN and reads the "scp" claim, which contains a scope
of the form "Actions.Results:<workflow_run_backend_id>:<workflow_job_run_backend_id>".

Without this derivation the IDs in the request body do not match the IDs the
backend tied to the bearer token, so authorization fails before authentication
proceeds.

Adds a base64-decoded JWT-payload parser (no signature verification - the token
is already trusted by virtue of being the runner's own token) and uses the
extracted IDs in CreateArtifact and FinalizeArtifact.
@kiwamizamurai
Copy link
Copy Markdown
Author

@s1ntaxe770r Got it tested end-to-end. Spinning up a real workflow surfaced two more bugs that the original commit didn't account for, so I've pushed two follow-up commits on top of the v4 migration:

1. action.ymlACTIONS_RESULTS_URL wasn't reaching the CLI

GitHub Actions only injects the runtime service env vars (ACTIONS_RUNTIME_TOKEN, ACTIONS_RESULTS_URL, …) into Node.js actions, not into shell run: steps. The digger CLI runs from a run: step and saw the variable as empty — the Go code aborted immediately with ACTIONS_RESULTS_URL is not set.

action.yml already had a core.exportVariable() step that re-exports the v1-v3 vars via \$GITHUB_ENV for this exact reason. I added ACTIONS_RESULTS_URL to the same list. One-line fix.

2. plan_storage.goworkflow_run_backend_id must come from the JWT, not from GITHUB_RUN_ID

After fixing #1, CreateArtifact started returning HTTP 401 \"invalid auth token\". The official @actions/artifact v2 doesn't use GITHUB_RUN_ID / GITHUB_RUN_ATTEMPT for these IDs — it parses the JWT in ACTIONS_RUNTIME_TOKEN and reads the `scp` claim, which encodes them as:

```
Actions.Results:<workflow_run_backend_id>:<workflow_job_run_backend_id>
```

The IDs the backend ties to the bearer token are baked into the token itself, so they have to be extracted from there. Added a small base64-decode-only JWT payload parser (no signature check — it's the runner's own token).

Verified end-to-end on a hosted runner:

```
level=INFO msg="Successfully stored plan file in GitHub artifacts"
owner=kiwamizamurai repo=digger_tutorial artifactName=sandbox size=2607
```

All three Twirp calls succeed (CreateArtifact → signed-URL PUT → FinalizeArtifact). Test repo PR for evidence: kiwamizamurai/digger_tutorial#4

PR description updated to cover all three pieces. Ready for another look 🙏

Adds black-box tests for the two pieces of new behavior introduced by the
v1-v3 → v4 migration:

- extractArtifactBackendIDs: pure-function coverage of the JWT-payload
  parser, including malformed segments, missing scope, and empty/wrong-
  shaped Actions.Results scope values.
- StorePlanFile: integration coverage of the 3-step Twirp + Azure-Blob
  upload flow against an httptest.Server, asserting request method, path,
  Authorization/Content-Type/x-ms-blob-* headers, and JSON body shape;
  plus negative paths for missing env vars, malformed token, server
  errors at each step, missing/empty/wrong-typed signed_upload_url,
  trailing slash on ACTIONS_RESULTS_URL, and empty file contents.

No production-code changes — the existing API is testable as-is via
t.Setenv and a loopback httptest.Server. Coverage:
extractArtifactBackendIDs 100%, StorePlanFile 100%, doRequest 75%
(remaining uncovered branches are http.NewRequest failure paths
unreachable from well-formed calls).
@s1ntaxe770r
Copy link
Copy Markdown
Contributor

Awesome i would try this out myself , do you have any steps to reproduce myself?

@kiwamizamurai
Copy link
Copy Markdown
Author

kiwamizamurai commented Apr 28, 2026

No? Maybe you can just refer my above pr?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

upload-plan-destination: github is using deprecated Artifact API scheduled for EOL on November 30, 2024

2 participants